/*
 * $Id: PriceServices.java,v 1.1 2008/02/01 02:58:18 linkconsulting Exp $
 *
 *  Copyright (c) 2001-2005 The Open For Business Project - www.ofbiz.org
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 *  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 *  CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
 *  OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
 *  THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.ofbiz.product.price;

import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import javolution.util.FastList;
import javolution.util.FastMap;

import org.apache.commons.collections.FastArrayList;
import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilDateTime;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilProperties;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.entity.GenericDelegator;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityConditionList;
import org.ofbiz.entity.condition.EntityExpr;
import org.ofbiz.entity.condition.EntityOperator;
import org.ofbiz.entity.util.EntityUtil;
import org.ofbiz.product.product.ProductWorker;
import org.ofbiz.service.DispatchContext;
import org.ofbiz.service.GenericServiceException;
import org.ofbiz.service.LocalDispatcher;
import org.ofbiz.service.ServiceUtil;

/**
 * PriceServices - Workers and Services class for product price related functionality
 *
 * @author     <a href="mailto:jonesde@ofbiz.org">David E. Jones</a>
 * @author     <a href="mailto:jaz@ofbiz.org">Andy Zeneski</a>
 * @version    $Rev: 6307 $
 * @since      2.0
 */
public class PriceServices {

    public static final String module = PriceServices.class.getName();
    public static final BigDecimal ONE_BASE = new BigDecimal("1.000"); 
    public static final BigDecimal PERCENT_SCALE = new BigDecimal("100.000"); 

    /**
     * <p>Calculates the price of a product from pricing rules given the following input, and of course access to the database:</p>
     * <ul>
     *   <li>productId
     *   <li>partyId
     *   <li>prodCatalogId
     *   <li>webSiteId
     *   <li>productStoreGroupId
     *   <li>quantity
     * </ul>
     */
    public static Map calculateProductPrice(DispatchContext dctx, Map context) {
        boolean optimizeForLargeRuleSet = false;

        // UtilTimer utilTimer = new UtilTimer();
        // utilTimer.timerString("Starting price calc", module);
        // utilTimer.setLog(false);

        System.out.println("Recalculating");
        
        GenericDelegator delegator = dctx.getDelegator();
        LocalDispatcher dispatcher = dctx.getDispatcher();
        Map result = new HashMap();
        Timestamp momentFrom = (Timestamp)context.get("reservFrom");

        Timestamp momentThru = (Timestamp)context.get("reservThru");
        
        if (momentThru == null && momentFrom == null){
        	momentFrom = UtilDateTime.nowTimestamp();
        	momentThru = UtilDateTime.nowTimestamp();
        } else if(momentThru==null && momentFrom !=null){
        	Double length = (Double)context.get("reservLength");
        	if (length ==null){
        		momentThru = UtilDateTime.nowTimestamp();
        	} else {
        		GregorianCalendar cal = new GregorianCalendar();
        		cal.setTime(momentFrom);
        		cal.add(Calendar.DATE, length.intValue());
        		momentThru = new Timestamp(cal.getTimeInMillis());
        	}
        }
        
        boolean isSale = false;
        List orderItemPriceInfos = new LinkedList();

        GenericValue product = (GenericValue) context.get("product");
        String productId = product.getString("productId");
        String prodCatalogId = (String) context.get("prodCatalogId");
        String webSiteId = (String) context.get("webSiteId");
        String checkIncludeVat = (String) context.get("checkIncludeVat");
        
        String productStoreId = (String) context.get("productStoreId");
        String productStoreGroupId = (String) context.get("productStoreGroupId");
        
        // if we are dealing with a booking then lets get the interval of the
		// booking, like that we can get the right prices for the booking !!!
        
        GenericValue productStore = null;
        try {
            // we have a productStoreId, if the corresponding ProductStore.primaryStoreGroupId is not empty, use that
            productStore = delegator.findByPrimaryKeyCache("ProductStore", UtilMisc.toMap("productStoreId", productStoreId));
        } catch (GenericEntityException e) {
            String errMsg = "Error getting product store info from the database while calculating price" + e.toString();
            Debug.logError(e, errMsg, module);
            return ServiceUtil.returnError(errMsg);
        }
        if (UtilValidate.isEmpty(productStoreGroupId)) {
            if (productStore != null) {
                try {
                    if (UtilValidate.isNotEmpty(productStore.getString("primaryStoreGroupId"))) {
                        productStoreGroupId = productStore.getString("primaryStoreGroupId");
                    } else {
                        // no ProductStore.primaryStoreGroupId, try ProductStoreGroupMember
                        List productStoreGroupMemberList = delegator.findByAndCache("ProductStoreGroupMember", UtilMisc.toMap("productStoreId", productStoreId), UtilMisc.toList("sequenceNum", "-fromDate"));
                        productStoreGroupMemberList = EntityUtil.filterByDate(productStoreGroupMemberList, true);
                        if (productStoreGroupMemberList.size() > 0) {
                            GenericValue productStoreGroupMember = EntityUtil.getFirst(productStoreGroupMemberList);
                            productStoreGroupId = productStoreGroupMember.getString("productStoreGroupId");
                        }
                    }
                } catch (GenericEntityException e) {
                    String errMsg = "Error getting product store info from the database while calculating price" + e.toString();
                    Debug.logError(e, errMsg, module);
                    return ServiceUtil.returnError(errMsg);
                }
            }

            // still empty, default to _NA_
            if (UtilValidate.isEmpty(productStoreGroupId)) {
                productStoreGroupId = "_NA_";
            }
        }
        
        // if currencyUomId is null get from properties file, if nothing there assume USD (USD: American Dollar) for now
        String currencyUomId = (String) context.get("currencyUomId");
        if (UtilValidate.isEmpty(currencyUomId)) {
            currencyUomId = UtilProperties.getPropertyValue("general", "currency.uom.id.default", "USD");
        }

        // productPricePurposeId is null assume "PURCHASE", which is equivalent to what prices were before the purpose concept
        String productPricePurposeId = (String) context.get("productPricePurposeId");
        if (UtilValidate.isEmpty(productPricePurposeId)) {
            productPricePurposeId = "PURCHASE";
        }
        
        // termUomId, for things like recurring prices specifies the term (time/frequency measure for example) of the recurrence
        // if this is empty it will simply not be used to constrain the selection
        String termUomId = (String) context.get("termUomId");

        // if this product is variant, find the virtual product and apply checks to it as well
        String virtualProductId = null;
        if ("Y".equals(product.getString("isVariant"))) {
            try {
                virtualProductId = ProductWorker.getVariantVirtualId(product);
            } catch (GenericEntityException e) {
                String errMsg = "Error getting virtual product id from the database while calculating price" + e.toString();
                Debug.logError(e, errMsg, module);
                return ServiceUtil.returnError(errMsg);
            }
        }

        // get prices for virtual product if one is found; get all ProductPrice entities for this productId and currencyUomId
        List virtualProductPrices = null;
        if (virtualProductId != null) {
            try {
                virtualProductPrices = delegator.findByAndCache("ProductPrice", UtilMisc.toMap("productId", virtualProductId, "currencyUomId", currencyUomId, "productStoreGroupId", productStoreGroupId), UtilMisc.toList("-fromDate"));
            } catch (GenericEntityException e) {
                Debug.logError(e, "An error occurred while getting the product prices", module);
            }
            virtualProductPrices = EntityUtil.filterByDate(virtualProductPrices, true);
        }

        // NOTE: partyId CAN be null
        String partyId = (String) context.get("partyId");
        if (UtilValidate.isEmpty(partyId) && context.get("userLogin") != null) {
            GenericValue userLogin = (GenericValue) context.get("userLogin");
            partyId = userLogin.getString("partyId");
        }

        // check for auto-userlogin for price rules
        if (UtilValidate.isEmpty(partyId) && context.get("autoUserLogin") != null) {
            GenericValue userLogin = (GenericValue) context.get("autoUserLogin");
            partyId = userLogin.getString("partyId");
        }

        Double quantityDbl = (Double) context.get("quantity");
        if (quantityDbl == null) quantityDbl = new Double(1.0);
        double quantity = quantityDbl.doubleValue();
        
        List productPriceEcList = FastList.newInstance();
        productPriceEcList.add(new EntityExpr("productId", EntityOperator.EQUALS, productId));
        // this funny statement is for backward compatibility purposes; the productPricePurposeId is a new pk field on the ProductPrice entity and in order databases may not be populated, until the pk is updated and such; this will ease the transition somewhat 
        if ("PURCHASE".equals(productPricePurposeId)) {
            productPriceEcList.add(new EntityExpr(
                    new EntityExpr("productPricePurposeId", EntityOperator.EQUALS, productPricePurposeId), 
                    EntityOperator.OR, 
                    new EntityExpr("productPricePurposeId", EntityOperator.EQUALS, null)));
        } else {
            productPriceEcList.add(new EntityExpr("productPricePurposeId", EntityOperator.EQUALS, productPricePurposeId));
        }
        productPriceEcList.add(new EntityExpr("currencyUomId", EntityOperator.EQUALS, currencyUomId));
        productPriceEcList.add(new EntityExpr("productStoreGroupId", EntityOperator.EQUALS, productStoreGroupId));
        if (UtilValidate.isNotEmpty(termUomId)) {
            productPriceEcList.add(new EntityExpr("termUomId", EntityOperator.EQUALS, termUomId));
        }
        EntityCondition productPriceEc = new EntityConditionList(productPriceEcList, EntityOperator.AND);

        // for prices, get all ProductPrice entities for this productId and currencyUomId
        List productPrices = null;
        try {
            productPrices = delegator.findByConditionCache("ProductPrice", productPriceEc, null, UtilMisc.toList("-fromDate"));
        } catch (GenericEntityException e) {
            Debug.logError(e, "An error occurred while getting the product prices", module);
        }
        productPrices = EntityUtil.filterByDate(productPrices, momentFrom, momentThru, null, null, true, true);

        GenericValue listPriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "LIST_PRICE");
        
        GenericValue defaultPriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "DEFAULT_PRICE");
        
        GenericValue competitivePriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "COMPETITIVE_PRICE");
        
        GenericValue averageCostValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "AVERAGE_COST");
        
        GenericValue promoPriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "PROMO_PRICE");
        
        GenericValue minimumPriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "MINIMUM_PRICE");
        
        GenericValue maximumPriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "MAXIMUM_PRICE");
        
        GenericValue wholesalePriceValue = getPriceSingleByType(currencyUomId, productId, productPrices,  "WHOLESALE_PRICE");
        
		List calendarPrices = EntityUtil.filterByAnd(productPrices, UtilMisc.toMap("productPriceTypeId", "CALENDAR_PRICE"));
        
        // if any of these prices is missing and this product is a variant, default to the corresponding price on the virtual product
        if (virtualProductPrices != null && virtualProductPrices.size() > 0) {
            listPriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, listPriceValue, "LIST_PRICE");
            
            defaultPriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, defaultPriceValue, "DEFAULT_PRICE");
            
            competitivePriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, competitivePriceValue, "COMPETITIVE_PRICE");
            
            averageCostValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, averageCostValue, "AVERAGE_COST");
            
            promoPriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, promoPriceValue, "PROMO_PRICE");
            
            minimumPriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, minimumPriceValue, "MINIMUM_PRICE");
            
            maximumPriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, maximumPriceValue, "MAXIMUM_PRICE");
            
            wholesalePriceValue = resetPriceSingleByType(currencyUomId, virtualProductId, virtualProductPrices, wholesalePriceValue, "WHOLESALE_PRICE");
            
    		calendarPrices = calendarPrices!=null?calendarPrices:EntityUtil.filterByAnd(virtualProductPrices, UtilMisc.toMap("productPriceTypeId", "CALENDAR_PRICE"));
        }

        // now if this is a virtual product check each price type, if doesn't exist get from variant with lowest DEFAULT_PRICE
        if ("Y".equals(product.getString("isVirtual"))) {
                // only do this if there is no default price, consider the others optional for performance reasons
                if (defaultPriceValue == null) {
                    // Debug.logInfo("Product isVirtual and there is no default price for ID " + productId + ", trying variant prices", module);

                    //use the cache to find the variant with the lowest default price
                    try {
                            List variantAssocList = EntityUtil.filterByDate(delegator.findByAndCache("ProductAssoc", UtilMisc.toMap("productId", product.get("productId"), "productAssocTypeId", "PRODUCT_VARIANT"), UtilMisc.toList("-fromDate")));
                            Iterator variantAssocIter = variantAssocList.iterator();
                            double minDefaultPrice = Double.MAX_VALUE;
                            List variantProductPrices = null;
                            String variantProductId = null;
                            
                            while (variantAssocIter.hasNext()) {
                                GenericValue variantAssoc = (GenericValue) variantAssocIter.next();
                                String curVariantProductId = variantAssoc.getString("productIdTo");
                                List curVariantPriceList = EntityUtil.filterByDate(delegator.findByAndCache("ProductPrice", UtilMisc.toMap("productId", curVariantProductId), UtilMisc.toList("-fromDate")), momentFrom, momentThru);
                                List tempDefaultPriceList = EntityUtil.filterByAnd(curVariantPriceList, UtilMisc.toMap("productPriceTypeId", "DEFAULT_PRICE"));
                                GenericValue curDefaultPriceValue = EntityUtil.getFirst(tempDefaultPriceList);
                                if (curDefaultPriceValue != null) {
                                    Double curDefaultPrice = curDefaultPriceValue.getDouble("price");
                                    if (curDefaultPrice.doubleValue() < minDefaultPrice) {
                                        // check to see if the product is discontinued for sale before considering it the lowest price
                                        GenericValue curVariantProduct = delegator.findByPrimaryKeyCache("Product", UtilMisc.toMap("productId", curVariantProductId));
                                        if (curVariantProduct != null) {
                                            Timestamp salesDiscontinuationDate = curVariantProduct.getTimestamp("salesDiscontinuationDate");
                                            if (salesDiscontinuationDate == null || salesDiscontinuationDate.after(momentThru)) {
                                                minDefaultPrice = curDefaultPrice.doubleValue();
                                                variantProductPrices = curVariantPriceList;
                                                variantProductId = curVariantProductId;
                                                // Debug.logInfo("Found new lowest price " + minDefaultPrice + " for variant with ID " + variantProductId, module);
                                            }
                                        }
                                    }
                                }
                            }

                            if (variantProductPrices != null) {
                                // we have some other options, give 'em a go...
                                listPriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, listPriceValue, "LIST_PRICE");
                                
                                defaultPriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, defaultPriceValue, "DEFAULT_PRICE");
                                
                                competitivePriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, competitivePriceValue, "COMPETITIVE_PRICE");
                                
                                averageCostValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, averageCostValue, "AVERAGE_COST");
                                
                                promoPriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, promoPriceValue, "PROMO_PRICE");
                                
                                minimumPriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, minimumPriceValue, "MINIMUM_PRICE");
                                
                                maximumPriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, maximumPriceValue, "MAXIMUM_PRICE");
                                
                                wholesalePriceValue = resetPriceSingleByType(currencyUomId, variantProductId, variantProductPrices, wholesalePriceValue, "WHOLESALE_PRICE");
                                
                        		calendarPrices = calendarPrices!=null?calendarPrices:EntityUtil.filterByAnd(variantProductPrices, UtilMisc.toMap("productPriceTypeId", "CALENDAR_PRICE"));

                            }
                    } catch (GenericEntityException e) {
                        Debug.logError(e, "An error occurred while getting the product prices", module);
                    }
                }
        }

        //boolean validPromoPriceFound = false;
        double promoPrice = 0;
        if (promoPriceValue != null && promoPriceValue.get("price") != null) {
            promoPrice = promoPriceValue.getDouble("price").doubleValue();
            //validPromoPriceFound = true;
        }

        //boolean validWholesalePriceFound = false;
        double wholesalePrice = 0;
        if (wholesalePriceValue != null && wholesalePriceValue.get("price") != null) {
            wholesalePrice = wholesalePriceValue.getDouble("price").doubleValue();
            //validWholesalePriceFound = true;
        }

        boolean validPriceFound = false;
        double defaultPrice = 0;
        if (defaultPriceValue != null && defaultPriceValue.get("price") != null) {
            defaultPrice = defaultPriceValue.getDouble("price").doubleValue();
            validPriceFound = true;
        }

        Double listPriceDbl = listPriceValue != null ? listPriceValue.getDouble("price") : null;

        if(calendarPrices!=null && !calendarPrices.isEmpty() && !momentFrom.equals(momentThru) ){
        	// CALCULATE BASED ON CALENDAR PRICES!!!
        	// AJL 2006-04-24
        	// THIS MAY NOT BE THE BEST PLACE... MAYBE WE SHOULD INJECT A LIST_PRICE 
        	// instead but for now this will give us what we want RIGHT now

        	double calendarPrice = 0.0;
        	Iterator iterator = calendarPrices.iterator();
        	List calPrices = new FastArrayList();
    		System.out.println("»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»");

    		Calendar calendarEnd = new GregorianCalendar();
    		calendarEnd.setTime(momentThru);
    		calendarEnd.add(Calendar.DATE,-1);
    		
    		java.util.Date momentCalcStart = new Date(momentFrom.getTime());
    		java.util.Date momentCalcEnd = new Date(calendarEnd.getTimeInMillis()); 
    		
        	while(iterator.hasNext()){
        		GenericValue price = (GenericValue)iterator.next();
        		java.util.Date start = (Timestamp)price.get("fromDate");
        		java.util.Date end = (Timestamp)price.get("thruDate");

        		if (start== null||start.before(momentCalcStart)){
        			start = momentCalcStart;
        		} else {
        			start = new Date(start.getTime());
        		}

        		if(end==null || end.after(momentCalcEnd)){
        			end = momentCalcEnd;
        		} else {
        			end = new Date(end.getTime());
        		}

        		Date dStart = new Date(start.getTime());
        		Date dEnd = new Date(end.getTime());

        		long lLength = dEnd.getTime() - dStart.getTime();

        		// This seems to function correctly to obtain the difference in
				// days taking into account the 23hr and 25hr days
        		int iLength = 1 + (int) Math
						.ceil((((double) lLength) - (double) (1 * 60 * 60 * 1000))
								/ (double) (24 * 60 * 60 * 1000));

        		Double dblPrice = price.getDouble("price");
        		calendarPrice += iLength * dblPrice.doubleValue();

        		Calendar displayEnd = new GregorianCalendar();
        		displayEnd.setTime(end);
        		displayEnd.add(Calendar.DATE,1);

        		end =  new Date(displayEnd.getTimeInMillis());

        		Map calPrice = new FastMap();
        		calPrice.put("start", start);
        		calPrice.put("end", end);
        		calPrice.put("price", dblPrice);
        		calPrice.put("length", new Integer(iLength));
        		calPrice.put("total", new Double(iLength * dblPrice.doubleValue()));
        		calPrices.add(calPrice);

        		System.out.println(start + " - " + end + ", " + iLength + ", " + dblPrice);
        	}
    		System.out.println("»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»»");
            validPriceFound = true;
        	result.put("calendarPrices", calPrices );
            result.put("basePrice", new Double(calendarPrice));
            result.put("price", new Double(calendarPrice));
            result.put("defaultPrice", new Double(calendarPrice));

        }else if (listPriceDbl == null) {
            // no list price, use defaultPrice for the final price

            // ========= ensure calculated price is not below minSalePrice or above maxSalePrice =========
            Double maxSellPrice = maximumPriceValue != null ? maximumPriceValue.getDouble("price") : null;
            if (maxSellPrice != null && defaultPrice > maxSellPrice.doubleValue()) {
                defaultPrice = maxSellPrice.doubleValue();
            }
            // min price second to override max price, safety net
            Double minSellPrice = minimumPriceValue != null ? minimumPriceValue.getDouble("price") : null;
            if (minSellPrice != null && defaultPrice < minSellPrice.doubleValue()) {
                defaultPrice = minSellPrice.doubleValue();
                // since we have found a minimum price that has overriden a the defaultPrice, even if no valid one was found, we will consider it as if one had been...
                validPriceFound = true;
            }

            result.put("basePrice", new Double(defaultPrice));
            result.put("price", new Double(defaultPrice));
            result.put("defaultPrice", new Double(defaultPrice));
            result.put("competitivePrice", competitivePriceValue != null ? competitivePriceValue.getDouble("price") : null);
            result.put("averageCost", averageCostValue != null ? averageCostValue.getDouble("price") : null);
            result.put("promoPrice", promoPriceValue != null ? promoPriceValue.getDouble("price") : null);
            
        } else {
            try {
                // get some of the base values to calculate with
                double listPrice = listPriceDbl.doubleValue();
                double averageCost = (averageCostValue != null && averageCostValue.get("price") != null) ? averageCostValue.getDouble("price").doubleValue() : listPrice;
                double margin = listPrice - averageCost;

                // calculate running sum based on listPrice and rules found
                double price = listPrice;

                Collection productPriceRules = null;

                // At this point we have two options: optimize for large ruleset, or optimize for small ruleset
                // NOTE: This only effects the way that the rules to be evaluated are selected.
                // For large rule sets we can do a cached pre-filter to limit the rules that need to be evaled for a specific product.
                // Genercally I don't think that rule sets will get that big though, so the default is optimize for smaller rule set.
                if (optimizeForLargeRuleSet) {
                    productPriceRules = fetchPriceRules(delegator, productId, prodCatalogId, webSiteId, productStoreGroupId, currencyUomId, virtualProductId, partyId);
                } else {
                    // this would be nice, but we can't cache this so easily...
                    // List pprExprs = UtilMisc.toList(new EntityExpr("thruDate", EntityOperator.EQUALS, null),
                    // new EntityExpr("thruDate", EntityOperator.GREATER_THAN, UtilDateTime.nowTimestamp()));
                    // productPriceRules = delegator.findByOr("ProductPriceRule", pprExprs);

                    productPriceRules = delegator.findAllCache("ProductPriceRule");
                    if (productPriceRules == null) productPriceRules = new LinkedList();
                }

                // ========= go through each price rule by id and eval all conditions =========
                // utilTimer.timerString("Before eval rules", module);
                int totalConds = 0;
                int totalActions = 0;
                int totalRules = 0;

                Iterator productPriceRulesIter = productPriceRules.iterator();
                while (productPriceRulesIter.hasNext()) {
                    GenericValue productPriceRule = (GenericValue) productPriceRulesIter.next();
                    String productPriceRuleId = productPriceRule.getString("productPriceRuleId");

                    // check from/thru dates
                    java.sql.Timestamp fromDate = productPriceRule.getTimestamp("fromDate");
                    java.sql.Timestamp thruDate = productPriceRule.getTimestamp("thruDate");

                    if (fromDate != null && fromDate.after(momentFrom)) {
                        // hasn't started yet
                        continue;
                    }
                    if (thruDate != null && thruDate.before(momentThru)) {
                        // already expired
                        continue;
                    }

                    // check all conditions
                    boolean allTrue = true;
                    StringBuffer condsDescription = new StringBuffer();
                    Collection productPriceConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("productPriceRuleId", productPriceRuleId));
                    Iterator productPriceCondsIter = UtilMisc.toIterator(productPriceConds);

                    while (productPriceCondsIter != null && productPriceCondsIter.hasNext()) {
                        GenericValue productPriceCond = (GenericValue) productPriceCondsIter.next();

                        totalConds++;

                        if (!checkPriceCondition(productPriceCond, productId, prodCatalogId, productStoreGroupId, webSiteId, partyId, quantity, listPrice, currencyUomId, delegator)) {
                            // if there is a virtualProductId, try that given that this one has failed
                            if (virtualProductId != null) {
                                if (!checkPriceCondition(productPriceCond, virtualProductId, prodCatalogId, productStoreGroupId, webSiteId, partyId, quantity, listPrice, currencyUomId, delegator)) {
                                    allTrue = false;
                                    break;
                                }
                                // otherwise, okay, this one made it so carry on checking
                            } else {
                                allTrue = false;
                                break;
                            }
                        }

                        // add condsDescription string entry
                        condsDescription.append("[");
                        GenericValue inputParamEnum = productPriceCond.getRelatedOneCache("InputParamEnumeration");

                        condsDescription.append(inputParamEnum.getString("enumCode"));
                        // condsDescription.append(":");
                        GenericValue operatorEnum = productPriceCond.getRelatedOneCache("OperatorEnumeration");

                        condsDescription.append(operatorEnum.getString("description"));
                        // condsDescription.append(":");
                        condsDescription.append(productPriceCond.getString("condValue"));
                        condsDescription.append("] ");
                    }

                    // add some info about the prices we are calculating from
                    condsDescription.append("[list:");
                    condsDescription.append(listPrice);
                    condsDescription.append(";avgCost:");
                    condsDescription.append(averageCost);
                    condsDescription.append(";margin:");
                    condsDescription.append(margin);
                    condsDescription.append("] ");

                    boolean foundFlatOverride = false;

                    // if all true, perform all actions
                    if (allTrue) {
                        // check isSale
                        if ("Y".equals(productPriceRule.getString("isSale"))) {
                            isSale = true;
                        }

                        Collection productPriceActions = delegator.findByAndCache("ProductPriceAction", UtilMisc.toMap("productPriceRuleId", productPriceRuleId));
                        Iterator productPriceActionsIter = UtilMisc.toIterator(productPriceActions);

                        while (productPriceActionsIter != null && productPriceActionsIter.hasNext()) {
                            GenericValue productPriceAction = (GenericValue) productPriceActionsIter.next();

                            totalActions++;

                            // yeah, finally here, perform the action, ie, modify the price
                            double modifyAmount = 0;

                            if ("PRICE_POD".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                if (productPriceAction.get("amount") != null) {
                                    modifyAmount = defaultPrice * (productPriceAction.getDouble("amount").doubleValue() / 100.0);
                                }
                            } else if ("PRICE_POL".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                if (productPriceAction.get("amount") != null) {
                                    modifyAmount = listPrice * (productPriceAction.getDouble("amount").doubleValue() / 100.0);
                                }
                            } else if ("PRICE_POAC".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                if (productPriceAction.get("amount") != null) {
                                    modifyAmount = averageCost * (productPriceAction.getDouble("amount").doubleValue() / 100.0);
                                }
                            } else if ("PRICE_POM".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                if (productPriceAction.get("amount") != null) {
                                    modifyAmount = margin * (productPriceAction.getDouble("amount").doubleValue() / 100.0);
                                }
                            } else if ("PRICE_FOL".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                if (productPriceAction.get("amount") != null) {
                                    modifyAmount = productPriceAction.getDouble("amount").doubleValue();
                                }
                            } else if ("PRICE_FLAT".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                // this one is a bit different, break out of the loop because we now have our final price
                                foundFlatOverride = true;
                                if (productPriceAction.get("amount") != null) {
                                    price = productPriceAction.getDouble("amount").doubleValue();
                                } else {
                                    Debug.logInfo("ProductPriceAction had null amount, using default price: " + defaultPrice + " for product with id " + productId, module);
                                    price = defaultPrice;
                                    isSale = false;                // reverse isSale flag, as this sale rule was actually not applied
                                }
                            } else if ("PRICE_PFLAT".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                // this one is a bit different too, break out of the loop because we now have our final price
                                foundFlatOverride = true;
                                price = promoPrice;
                                if (productPriceAction.get("amount") != null) {
                                    price += productPriceAction.getDouble("amount").doubleValue();
                                }
                                if (price == 0.00) {
                                    if (defaultPrice != 0.00) {
                                        Debug.logInfo("PromoPrice and ProductPriceAction had null amount, using default price: " + defaultPrice + " for product with id " + productId, module);
                                        price = defaultPrice;
                                    } else if (listPrice != 0.00) {
                                        Debug.logInfo("PromoPrice and ProductPriceAction had null amount and no default price was available, using list price: " + listPrice + " for product with id " + productId, module);
                                        price = listPrice;
                                    } else {
                                        Debug.logError("PromoPrice and ProductPriceAction had null amount and no default or list price was available, so price is set to zero for product with id " + productId, module);
                                        price = 0.00;
                                    }
                                    isSale = false;                // reverse isSale flag, as this sale rule was actually not applied
                                }
                            } else if ("PRICE_WFLAT".equals(productPriceAction.getString("productPriceActionTypeId"))) {
                                // same as promo price but using the wholesale price instead
                                foundFlatOverride = true;
                                price = wholesalePrice;
                                if (productPriceAction.get("amount") != null) {
                                    price += productPriceAction.getDouble("amount").doubleValue();
                                }
                                if (price == 0.00) {
                                    if (defaultPrice != 0.00) {
                                        Debug.logInfo("WholesalePrice and ProductPriceAction had null amount, using default price: " + defaultPrice + " for product with id " + productId, module);
                                        price = defaultPrice;
                                    } else if (listPrice != 0.00) {
                                        Debug.logInfo("WholesalePrice and ProductPriceAction had null amount and no default price was available, using list price: " + listPrice + " for product with id " + productId, module);
                                        price = listPrice;
                                    } else {
                                        Debug.logError("WholesalePrice and ProductPriceAction had null amount and no default or list price was available, so price is set to zero for product with id " + productId, module);
                                        price = 0.00;
                                    }
                                    isSale = false; // reverse isSale flag, as this sale rule was actually not applied
                                }
                            }

                            // add a orderItemPriceInfo element too, without orderId or orderItemId
                            StringBuffer priceInfoDescription = new StringBuffer();

                            priceInfoDescription.append(condsDescription.toString());
                            priceInfoDescription.append("[type:");
                            priceInfoDescription.append(productPriceAction.getString("productPriceActionTypeId"));
                            priceInfoDescription.append("]");

                            GenericValue orderItemPriceInfo = delegator.makeValue("OrderItemPriceInfo", null);

                            orderItemPriceInfo.set("productPriceRuleId", productPriceAction.get("productPriceRuleId"));
                            orderItemPriceInfo.set("productPriceActionSeqId", productPriceAction.get("productPriceActionSeqId"));
                            orderItemPriceInfo.set("modifyAmount", new Double(modifyAmount));
                            // make sure description is <= than 250 chars
                            String priceInfoDescriptionString = priceInfoDescription.toString();

                            if (priceInfoDescriptionString.length() > 250) {
                                priceInfoDescriptionString = priceInfoDescriptionString.substring(0, 250);
                            }
                            orderItemPriceInfo.set("description", priceInfoDescriptionString);
                            orderItemPriceInfos.add(orderItemPriceInfo);

                            if (foundFlatOverride) {
                                break;
                            } else {
                                price += modifyAmount;
                            }
                        }
                    }

                    totalRules++;

                    if (foundFlatOverride) {
                        break;
                    }
                }

                if (Debug.verboseOn()) {
                    Debug.logVerbose("Unchecked Calculated price: " + price, module);
                    Debug.logVerbose("PriceInfo:", module);
                    Iterator orderItemPriceInfosIter = orderItemPriceInfos.iterator();
                    while (orderItemPriceInfosIter.hasNext()) {
                        GenericValue orderItemPriceInfo = (GenericValue) orderItemPriceInfosIter.next();

                        Debug.logVerbose(" --- " + orderItemPriceInfo.toString(), module);
                    }
                }

                // if no actions were run on the list price, then use the default price
                if (totalActions == 0) {
                    price = defaultPrice;
                    // here we will leave validPriceFound as it was originally set for the defaultPrice since that is what we are setting the price to...
                } else {
                    // at least one price rule action was found, so we will consider it valid
                    validPriceFound = true;
                }

                // ========= ensure calculated price is not below minSalePrice or above maxSalePrice =========
                Double maxSellPrice = maximumPriceValue != null ? maximumPriceValue.getDouble("price") : null;
                if (maxSellPrice != null && price > maxSellPrice.doubleValue()) {
                    price = maxSellPrice.doubleValue();
                }
                // min price second to override max price, safety net
                Double minSellPrice = minimumPriceValue != null ? minimumPriceValue.getDouble("price") : null;
                if (minSellPrice != null && price < minSellPrice.doubleValue()) {
                    price = minSellPrice.doubleValue();
                    // since we have found a minimum price that has overriden a the defaultPrice, even if no valid one was found, we will consider it as if one had been...
                    validPriceFound = true;
                }

                if (Debug.verboseOn()) Debug.logVerbose("Final Calculated price: " + price + ", rules: " + totalRules + ", conds: " + totalConds + ", actions: " + totalActions, module);

                result.put("basePrice", new Double(price));
                result.put("price", new Double(price));
                result.put("listPrice", new Double(listPrice));
                result.put("defaultPrice", new Double(defaultPrice));
                result.put("averageCost", new Double(averageCost));
            } catch (GenericEntityException e) {
                Debug.logError(e, "Error getting rules from the database while calculating price", module);
                return ServiceUtil.returnError("Error getting rules from the database while calculating price: " + e.toString());
            }
        }

        result.put("competitivePrice", competitivePriceValue != null ? competitivePriceValue.getDouble("price") : null);
        result.put("orderItemPriceInfos", orderItemPriceInfos);
        result.put("isSale", new Boolean(isSale));
        result.put("validPriceFound", new Boolean(validPriceFound));
        result.put("currencyUsed", currencyUomId);

        // okay, now we have the calculated price, see if we should add in tax and if so do it
        if ("Y".equals(checkIncludeVat) && productStore != null && "Y".equals(productStore.getString("showPricesWithVatTax"))) {
            Map calcTaxForDisplayContext = UtilMisc.toMap("productStoreId", productStoreId, 
                    "productId", productId, "quantity", new BigDecimal(quantity), 
                    "basePrice", new BigDecimal(((Double) result.get("price")).doubleValue()));
            if (UtilValidate.isNotEmpty(partyId)) {
                calcTaxForDisplayContext.put("billToPartyId", partyId);
            }
            
            try {
                Map calcTaxForDisplayResult = dispatcher.runSync("calcTaxForDisplay", calcTaxForDisplayContext);
                if (ServiceUtil.isError(calcTaxForDisplayResult)) {
                    return ServiceUtil.returnError("Error calculating VAT tax (with calcTaxForDisplay service)", null, null, calcTaxForDisplayResult);
                }
                // taxTotal, taxPercentage, priceWithTax
                result.put("price", new Double(((BigDecimal) calcTaxForDisplayResult.get("priceWithTax")).doubleValue()));

                // based on the taxPercentage calculate the other amounts, including: listPrice, defaultPrice, averageCost, promoPrice, competitivePrice
                BigDecimal taxPercentage = (BigDecimal) calcTaxForDisplayResult.get("taxPercentage");
                BigDecimal taxMultiplier = ONE_BASE.add(taxPercentage.divide(PERCENT_SCALE, 3));
                if (result.get("listPrice") != null) result.put("listPrice", new Double((new BigDecimal (((Double) result.get("listPrice")).doubleValue())).multiply(taxMultiplier).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()));
                if (result.get("defaultPrice") != null) result.put("defaultPrice", new Double((new BigDecimal (((Double) result.get("defaultPrice")).doubleValue())).multiply(taxMultiplier).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()));
                if (result.get("averageCost") != null) result.put("averageCost", new Double((new BigDecimal (((Double) result.get("averageCost")).doubleValue())).multiply(taxMultiplier).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()));
                
                if (result.get("promoPrice") != null) result.put("promoPrice", new Double((new BigDecimal (((Double) result.get("promoPrice")).doubleValue())).multiply(taxMultiplier).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()));
                if (result.get("competitivePrice") != null) result.put("competitivePrice", new Double((new BigDecimal (((Double) result.get("competitivePrice")).doubleValue())).multiply(taxMultiplier).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue()));
                
            } catch (GenericServiceException e) {
                String errMsg = "Error calculating VAT tax (with calcTaxForDisplay service): " + e.toString();
                Debug.logError(e, errMsg, module);
                return ServiceUtil.returnError(errMsg);
            }
        }

        // utilTimer.timerString("Finished price calc [productId=" + productId + "]", module);
        return result;
    }

	private static Collection fetchPriceRules(GenericDelegator delegator, String productId, String prodCatalogId, String webSiteId, String productStoreGroupId, String currencyUomId, String virtualProductId, String partyId) throws GenericEntityException {
		Collection productPriceRules;
		// ========= find all rules that must be run for each input type; this is kind of like a pre-filter to slim down the rules to run =========
		// utilTimer.timerString("Before create rule id list", module);
		TreeSet productPriceRuleIds = new TreeSet();

		// ------- These are all of the conditions that DON'T depend on the current inputs -------

		// by productCategoryId
		// for we will always include any rules that go by category, shouldn't be too many to iterate through each time and will save on cache entries
		// note that we always want to put the category, quantity, etc ones that find all rules with these conditions in separate cache lists so that they can be easily cleared
		Collection productCategoryIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_PROD_CAT_ID"));
		if (productCategoryIdConds != null && productCategoryIdConds.size() > 0) {
		    Iterator productCategoryIdCondsIter = productCategoryIdConds.iterator();
		    while (productCategoryIdCondsIter.hasNext()) {
		        GenericValue productCategoryIdCond = (GenericValue) productCategoryIdCondsIter.next();
		        productPriceRuleIds.add(productCategoryIdCond.getString("productPriceRuleId"));
		    }
		}

		// by quantity -- should we really do this one, ie is it necessary?
		// we could say that all rules with quantity on them must have one of these other values
		// but, no we'll do it the other way, any that have a quantity will always get compared
		Collection quantityConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_QUANTITY"));
		if (quantityConds != null && quantityConds.size() > 0) {
		    Iterator quantityCondsIter = quantityConds.iterator();
		    while (quantityCondsIter.hasNext()) {
		        GenericValue quantityCond = (GenericValue) quantityCondsIter.next();
		        productPriceRuleIds.add(quantityCond.getString("productPriceRuleId"));
		    }
		}

		// by roleTypeId
		Collection roleTypeIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_ROLE_TYPE"));
		if (roleTypeIdConds != null && roleTypeIdConds.size() > 0) {
		    Iterator roleTypeIdCondsIter = roleTypeIdConds.iterator();
		    while (roleTypeIdCondsIter.hasNext()) {
		        GenericValue roleTypeIdCond = (GenericValue) roleTypeIdCondsIter.next();
		        productPriceRuleIds.add(roleTypeIdCond.getString("productPriceRuleId"));
		    }
		}

		// TODO, not supported yet: by groupPartyId
		// TODO, not supported yet: by partyClassificationGroupId
		// later: (by partyClassificationTypeId)

		// by listPrice
		Collection listPriceConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_LIST_PRICE"));
		if (listPriceConds != null && listPriceConds.size() > 0) {
		    Iterator listPriceCondsIter = listPriceConds.iterator();
		    while (listPriceCondsIter.hasNext()) {
		        GenericValue listPriceCond = (GenericValue) listPriceCondsIter.next();
		        productPriceRuleIds.add(listPriceCond.getString("productPriceRuleId"));
		    }
		}

		// ------- These are all of them that DO depend on the current inputs -------

		// by productId
		Collection productIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_PRODUCT_ID", "condValue", productId));
		if (productIdConds != null && productIdConds.size() > 0) {
		    Iterator productIdCondsIter = productIdConds.iterator();
		    while (productIdCondsIter.hasNext()) {
		        GenericValue productIdCond = (GenericValue) productIdCondsIter.next();
		        productPriceRuleIds.add(productIdCond.getString("productPriceRuleId"));
		    }
		}

		// by virtualProductId, if not null
		if (virtualProductId != null) {
		    Collection virtualProductIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_PRODUCT_ID", "condValue", virtualProductId));
		    if (virtualProductIdConds != null && virtualProductIdConds.size() > 0) {
		        Iterator virtualProductIdCondsIter = virtualProductIdConds.iterator();
		        while (virtualProductIdCondsIter.hasNext()) {
		            GenericValue virtualProductIdCond = (GenericValue) virtualProductIdCondsIter.next();
		            productPriceRuleIds.add(virtualProductIdCond.getString("productPriceRuleId"));
		        }
		    }
		}

		// by prodCatalogId - which is optional in certain cases
		if (UtilValidate.isNotEmpty(prodCatalogId)) {
		    Collection prodCatalogIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_PROD_CLG_ID", "condValue", prodCatalogId));
		    if (prodCatalogIdConds != null && prodCatalogIdConds.size() > 0) {
		        Iterator prodCatalogIdCondsIter = prodCatalogIdConds.iterator();
		        while (prodCatalogIdCondsIter.hasNext()) {
		            GenericValue prodCatalogIdCond = (GenericValue) prodCatalogIdCondsIter.next();
		            productPriceRuleIds.add(prodCatalogIdCond.getString("productPriceRuleId"));
		        }
		    }
		}

		// by productStoreGroupId
		if (UtilValidate.isNotEmpty(productStoreGroupId)) {
		    Collection storeGroupConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_PROD_SGRP_ID", "condValue", productStoreGroupId));
		    if (storeGroupConds != null && storeGroupConds.size() > 0) {
		        Iterator storeGroupCondsIter = storeGroupConds.iterator();
		        while (storeGroupCondsIter.hasNext()) {
		            GenericValue storeGroupCond = (GenericValue) storeGroupCondsIter.next();
		            productPriceRuleIds.add(storeGroupCond.getString("productPriceRuleId"));
		        }
		    }
		}

		// by webSiteId
		if (UtilValidate.isNotEmpty(webSiteId)) {
		    Collection webSiteIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_WEBSITE_ID", "condValue", webSiteId));
		    if (webSiteIdConds != null && webSiteIdConds.size() > 0) {
		        Iterator webSiteIdCondsIter = webSiteIdConds.iterator();
		        while (webSiteIdCondsIter.hasNext()) {
		            GenericValue webSiteIdCond = (GenericValue) webSiteIdCondsIter.next();
		            productPriceRuleIds.add(webSiteIdCond.getString("productPriceRuleId"));
		        }
		    }
		}

		// by partyId
		if (UtilValidate.isNotEmpty(partyId)) {
		    Collection partyIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_PARTY_ID", "condValue", partyId));
		    if (partyIdConds != null && partyIdConds.size() > 0) {
		        Iterator partyIdCondsIter = partyIdConds.iterator();
		        while (partyIdCondsIter.hasNext()) {
		            GenericValue partyIdCond = (GenericValue) partyIdCondsIter.next();
		            productPriceRuleIds.add(partyIdCond.getString("productPriceRuleId"));
		        }
		    }
		}

		// by currencyUomId
		Collection currencyUomIdConds = delegator.findByAndCache("ProductPriceCond", UtilMisc.toMap("inputParamEnumId", "PRIP_CURRENCY_UOMID", "condValue", currencyUomId));
		if (currencyUomIdConds != null && currencyUomIdConds.size() > 0) {
		    Iterator currencyUomIdCondsIter = currencyUomIdConds.iterator();
		    while (currencyUomIdCondsIter.hasNext()) {
		        GenericValue currencyUomIdCond = (GenericValue) currencyUomIdCondsIter.next();
		        productPriceRuleIds.add(currencyUomIdCond.getString("productPriceRuleId"));
		    }
		}

		productPriceRules = new LinkedList();
		Iterator productPriceRuleIdsIter = productPriceRuleIds.iterator();
		while (productPriceRuleIdsIter.hasNext()) {
		    String productPriceRuleId = (String) productPriceRuleIdsIter.next();
		    GenericValue productPriceRule = delegator.findByPrimaryKeyCache("ProductPriceRule", UtilMisc.toMap("productPriceRuleId", productPriceRuleId));
		    if (productPriceRule == null) continue;
		    productPriceRules.add(productPriceRule);
		}
		return productPriceRules;
	}

	private static GenericValue resetPriceSingleByType(String currencyUomId, String productId, List productPrices, GenericValue priceValue, String priceTypeId) {
		if (priceValue == null) {
		    priceValue = getPriceSingleByType(currencyUomId, productId, productPrices, priceTypeId);
		}
		
		return priceValue;
	}

	private static GenericValue getPriceSingleByType(String currencyUomId, String productId, List productPrices, String priceTypeId) {
		GenericValue priceValue;
		List tempPrices = EntityUtil.filterByAnd(productPrices, UtilMisc.toMap("productPriceTypeId", priceTypeId));
		priceValue = EntityUtil.getFirst(tempPrices);
		if (tempPrices != null && tempPrices.size() > 1) {
		    if (Debug.infoOn()) Debug.logInfo("There is more than one " + priceTypeId + " with the currencyUomId " + currencyUomId + " and productId " + productId + ", using the latest found with price: " + priceValue.getDouble("price"), module);
		}
		return priceValue;
	}

    public static boolean checkPriceCondition(GenericValue productPriceCond, String productId, String prodCatalogId,
            String productStoreGroupId, String webSiteId, String partyId, double quantity, double listPrice,
            String currencyUomId, GenericDelegator delegator) throws GenericEntityException {
        if (Debug.verboseOn()) Debug.logVerbose("Checking price condition: " + productPriceCond, module);
        int compare = 0;

        if ("PRIP_PRODUCT_ID".equals(productPriceCond.getString("inputParamEnumId"))) {
            compare = productId.compareTo(productPriceCond.getString("condValue"));
        } else if ("PRIP_PROD_CAT_ID".equals(productPriceCond.getString("inputParamEnumId"))) {
            // if a ProductCategoryMember exists for this productId and the specified productCategoryId
            String productCategoryId = productPriceCond.getString("condValue");
            List productCategoryMembers = delegator.findByAndCache("ProductCategoryMember",
                    UtilMisc.toMap("productId", productId, "productCategoryId", productCategoryId));
            // and from/thru date within range
            productCategoryMembers = EntityUtil.filterByDate(productCategoryMembers, true);
            // then 0 (equals), otherwise 1 (not equals)
            if (productCategoryMembers != null && productCategoryMembers.size() > 0) {
                compare = 0;
            } else {
                compare = 1;
            }
        } else if ("PRIP_PROD_CLG_ID".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (UtilValidate.isNotEmpty(prodCatalogId)) {
                compare = prodCatalogId.compareTo(productPriceCond.getString("condValue"));
            } else {
                // this shouldn't happen because if prodCatalogId is null no PRIP_PROD_CLG_ID prices will be in the list
                compare = 1;
            }
        } else if ("PRIP_PROD_SGRP_ID".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (UtilValidate.isNotEmpty(productStoreGroupId)) {
                compare = productStoreGroupId.compareTo(productPriceCond.getString("condValue"));
            } else {
                compare = 1;
            }
        } else if ("PRIP_WEBSITE_ID".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (UtilValidate.isNotEmpty(webSiteId)) {
                compare = webSiteId.compareTo(productPriceCond.getString("condValue"));
            } else {
                compare = 1;
            }
        } else if ("PRIP_QUANTITY".equals(productPriceCond.getString("inputParamEnumId"))) {
            Double quantityValue = new Double(quantity);
            compare = quantityValue.compareTo(Double.valueOf(productPriceCond.getString("condValue")));
        } else if ("PRIP_PARTY_ID".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (UtilValidate.isNotEmpty(partyId)) {
                compare = partyId.compareTo(productPriceCond.getString("condValue"));
            } else {
                compare = 1;
            }
        } else if ("PRIP_PARTY_GRP_MEM".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (UtilValidate.isEmpty(partyId)) {
                compare = 1;
            } else {
                String groupPartyId = productPriceCond.getString("condValue");
                if (partyId.equals(groupPartyId)) {
                    compare = 0;
                } else {
                    // look for PartyRelationship with partyRelationshipTypeId=GROUP_ROLLUP, the partyIdTo is the group member, so the partyIdFrom is the groupPartyId
                    List partyRelationshipList = delegator.findByAndCache("PartyRelationship", UtilMisc.toMap("partyIdFrom", groupPartyId, "partyIdTo", partyId, "partyRelationshipTypeId", "GROUP_ROLLUP"));
                    // and from/thru date within range
                    partyRelationshipList = EntityUtil.filterByDate(partyRelationshipList, true);
                    // then 0 (equals), otherwise 1 (not equals)
                    if (partyRelationshipList != null && partyRelationshipList.size() > 0) {
                        compare = 0;
                    } else {
                        compare = 1;
                    }
                }
            }
        } else if ("PRIP_PARTY_CLASS".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (UtilValidate.isEmpty(partyId)) {
                compare = 1;
            } else {
                String partyClassificationGroupId = productPriceCond.getString("condValue");
                // find any PartyClassification
                List partyClassificationList = delegator.findByAndCache("PartyClassification", UtilMisc.toMap("partyId", partyId, "partyClassificationGroupId", partyClassificationGroupId));
                // and from/thru date within range
                partyClassificationList = EntityUtil.filterByDate(partyClassificationList, true);
                // then 0 (equals), otherwise 1 (not equals)
                if (partyClassificationList != null && partyClassificationList.size() > 0) {
                    compare = 0;
                } else {
                    compare = 1;
                }
            }
        } else if ("PRIP_ROLE_TYPE".equals(productPriceCond.getString("inputParamEnumId"))) {
            if (partyId != null) {
                // if a PartyRole exists for this partyId and the specified roleTypeId
                GenericValue partyRole = delegator.findByPrimaryKeyCache("PartyRole",
                        UtilMisc.toMap("partyId", partyId, "roleTypeId", productPriceCond.getString("condValue")));

                // then 0 (equals), otherwise 1 (not equals)
                if (partyRole != null) {
                    compare = 0;
                } else {
                    compare = 1;
                }
            } else {
                compare = 1;
            }
        } else if ("PRIP_LIST_PRICE".equals(productPriceCond.getString("inputParamEnumId"))) {
            Double listPriceValue = new Double(listPrice);

            compare = listPriceValue.compareTo(Double.valueOf(productPriceCond.getString("condValue")));
        } else if ("PRIP_CURRENCY_UOMID".equals(productPriceCond.getString("inputParamEnumId"))) {
            compare = currencyUomId.compareTo(productPriceCond.getString("condValue"));
        } else {
            Debug.logWarning("An un-supported productPriceCond input parameter (lhs) was used: " + productPriceCond.getString("inputParamEnumId") + ", returning false, ie check failed", module);
            return false;
        }

        if (Debug.verboseOn()) Debug.logVerbose("Price Condition compare done, compare=" + compare, module);

        if ("PRC_EQ".equals(productPriceCond.getString("operatorEnumId"))) {
            if (compare == 0) return true;
        } else if ("PRC_NEQ".equals(productPriceCond.getString("operatorEnumId"))) {
            if (compare != 0) return true;
        } else if ("PRC_LT".equals(productPriceCond.getString("operatorEnumId"))) {
            if (compare < 0) return true;
        } else if ("PRC_LTE".equals(productPriceCond.getString("operatorEnumId"))) {
            if (compare <= 0) return true;
        } else if ("PRC_GT".equals(productPriceCond.getString("operatorEnumId"))) {
            if (compare > 0) return true;
        } else if ("PRC_GTE".equals(productPriceCond.getString("operatorEnumId"))) {
            if (compare >= 0) return true;
        } else {
            Debug.logWarning("An un-supported productPriceCond condition was used: " + productPriceCond.getString("operatorEnumId") + ", returning false, ie check failed", module);
            return false;
        }
        return false;
    }

    /**
     * Calculates the purchase price of a product
     */
    public static Map calculatePurchasePrice(DispatchContext dctx, Map context) {

        GenericDelegator delegator = dctx.getDelegator();
        LocalDispatcher dispatcher = dctx.getDispatcher();
        Map result = new HashMap();

        List orderItemPriceInfos = new LinkedList();
        boolean validPriceFound = false;
        double price = 0.0;

        GenericValue product = (GenericValue)context.get("product");
        String productId = product.getString("productId");
        String currencyUomId = (String)context.get("currencyUomId");
        String partyId = (String)context.get("partyId");
        Double quantity = (Double)context.get("quantity");
        
        if (quantity == null) {
            quantity = new Double(1);
        }
        
        // TODO: a) Get the Price from the Agreement* data model
        //
        // b) If no price can be found, get the lastPrice from the SupplierProduct entity
        //
        Map priceContext = UtilMisc.toMap("currencyUomId", currencyUomId, "partyId", partyId, "productId", productId, "quantity", quantity);
        List productSuppliers = null;
        try {
            Map priceResult = dispatcher.runSync("getSuppliersForProduct", priceContext);
            if (ServiceUtil.isError(priceResult)) {
                String errMsg = ServiceUtil.getErrorMessage(priceResult);
                Debug.logError(errMsg, module);
                return ServiceUtil.returnError(errMsg);
            }
            productSuppliers = (List) priceResult.get("supplierProducts");
        } catch(GenericServiceException gse) {
            Debug.logError(gse, module);
            return ServiceUtil.returnError(gse.getMessage());
        }
        if ((productSuppliers != null) && (productSuppliers.size() > 0)) {
            GenericValue productSupplier = (GenericValue) productSuppliers.get(0);
            price = ((Double)productSupplier.get("lastPrice")).doubleValue();
            validPriceFound = true;
            // add a orderItemPriceInfo element too, without orderId or orderItemId
            StringBuffer priceInfoDescription = new StringBuffer();
            priceInfoDescription.append("SupplierProduct ");
            priceInfoDescription.append("[minimumOrderQuantity:");
            priceInfoDescription.append("" + productSupplier.getDouble("minimumOrderQuantity").doubleValue());
            priceInfoDescription.append("]");
            GenericValue orderItemPriceInfo = delegator.makeValue("OrderItemPriceInfo", null);
            //orderItemPriceInfo.set("productPriceRuleId", productPriceAction.get("productPriceRuleId"));
            //orderItemPriceInfo.set("productPriceActionSeqId", productPriceAction.get("productPriceActionSeqId"));
            //orderItemPriceInfo.set("modifyAmount", new Double(modifyAmount));
            // make sure description is <= than 250 chars
            String priceInfoDescriptionString = priceInfoDescription.toString();
            if (priceInfoDescriptionString.length() > 250) {
                priceInfoDescriptionString = priceInfoDescriptionString.substring(0, 250);
            }
            orderItemPriceInfo.set("description", priceInfoDescriptionString);
            orderItemPriceInfos.add(orderItemPriceInfo);
        }
        // TODO: c) If no price can be found, get the averageCost from the ProductPrice entity


        result.put("price", new Double(price));
        result.put("validPriceFound", new Boolean(validPriceFound));
        result.put("orderItemPriceInfos", orderItemPriceInfos);
        return result;
    }
}
