/* ===================================================================
 * RecipeBizImpl.java
 * 
 * Created Aug 4, 2004 11:18:39 AM
 * 
 * Copyright (c) 2004 Matt Magoffin (spamsqr@msqr.us)
 * 
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation; either version 2 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 
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
 * 02111-1307 USA
 * ===================================================================
 * $Id: RecipeBizImpl.java 86 2009-05-18 22:43:54Z msqr $
 * ===================================================================
 */

package magoffin.matt.ieat.biz.impl;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Calendar;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import magoffin.matt.ieat.EatValidationException;
import magoffin.matt.ieat.IllegalActionException;
import magoffin.matt.ieat.biz.BizContext;
import magoffin.matt.ieat.biz.DomainObjectFactory;
import magoffin.matt.ieat.biz.RecipeBiz;
import magoffin.matt.ieat.dao.BaseDao;
import magoffin.matt.ieat.dao.CourseDao;
import magoffin.matt.ieat.dao.DifficultyDao;
import magoffin.matt.ieat.dao.EthnicityDao;
import magoffin.matt.ieat.dao.IngredientDao;
import magoffin.matt.ieat.dao.MealDao;
import magoffin.matt.ieat.dao.PrepTimeDao;
import magoffin.matt.ieat.dao.RecipeDao;
import magoffin.matt.ieat.dao.SystemDao;
import magoffin.matt.ieat.dao.UnitDao;
import magoffin.matt.ieat.domain.Base;
import magoffin.matt.ieat.domain.Course;
import magoffin.matt.ieat.domain.Difficulty;
import magoffin.matt.ieat.domain.Ethnicity;
import magoffin.matt.ieat.domain.Ingredient;
import magoffin.matt.ieat.domain.Meal;
import magoffin.matt.ieat.domain.MealRecipe;
import magoffin.matt.ieat.domain.PrepTime;
import magoffin.matt.ieat.domain.Recipe;
import magoffin.matt.ieat.domain.RecipeComment;
import magoffin.matt.ieat.domain.RecipeIngredient;
import magoffin.matt.ieat.domain.RecipeRating;
import magoffin.matt.ieat.domain.System;
import magoffin.matt.ieat.domain.Unit;
import magoffin.matt.util.StringUtil;
import magoffin.matt.util.TemporaryFile;

import org.apache.commons.collections.SortedBag;
import org.apache.commons.collections.TreeBag;
import org.apache.log4j.Logger;
import org.springframework.beans.BeanUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

/**
 * Default implementation of RecipeBiz.
 * 
 * @author Matt Magoffin (spamsqr@msqr.us)
 * @version $Revision: 86 $ $Date: 2009-05-19 10:43:54 +1200 (Tue, 19 May 2009) $
 */
public class RecipeBizImpl implements RecipeBiz {
	
	private BaseDao baseDao;
	private CourseDao courseDao;
	private DifficultyDao difficultyDao;
	private EthnicityDao ethnicityDao;
	private IngredientDao ingredientDao;
	private MealDao mealDao;
	private PrepTimeDao prepTimeDao;
	private RecipeDao recipeDao;
	private SystemDao systemDao;
	private UnitDao unitDao;
	private Validator recipeValidator;
	private DomainObjectFactory domainObjectFactory;
	
	private String imageBasePath;
	private Set<String> imageSupportedExtensions = new HashSet<String>();
	
	/* Internally set properties below. */
	
	private File imageBaseDir;
	private final Logger log = Logger.getLogger(getClass());
	
	/**
	 * Initialize after properties configured.
	 */
	public void init() {
		if ( baseDao == null ) {
			throw new RuntimeException("baseDao not configured");
		}
		if ( courseDao == null ) {
			throw new RuntimeException("courseDao not configured");
		}
		if ( difficultyDao == null ) {
			throw new RuntimeException("difficultyDao not configured");
		}
		if ( ethnicityDao == null ) {
			throw new RuntimeException("ethnicityDao not configured");
		}
		if ( ingredientDao == null ) {
			throw new RuntimeException("ingredientDao not configured");
		}
		if ( mealDao == null ) {
			throw new RuntimeException("mealDao not configured");
		}
		if ( prepTimeDao == null ) {
			throw new RuntimeException("prepTimeDao not configured");
		}
		if ( recipeDao == null ) {
			throw new RuntimeException("recipeDao not configured");
		}
		if ( unitDao == null ) {
			throw new RuntimeException("unitDao not configured");
		}
		if ( recipeValidator == null ) {
			throw new RuntimeException("recipeValidator not configured");
		}
		if ( domainObjectFactory == null ) {
			throw new RuntimeException("domainObjectFactory not configured");
		}
		
		this.imageBaseDir = new File(imageBasePath);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getRecipeById(Long)
	 */
	public Recipe getRecipeById(Long recipeId) {
		return recipeDao.get(recipeId);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableUnitSystems()
	 */
	public List<System> getAvailableUnitSystems() {
		return systemDao.getSystems();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableBases()
	 */
	public List<Base> getAvailableBases() {
		return baseDao.getBases();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableCourses()
	 */
	public List<Course> getAvailableCourses() {
		return courseDao.getCourses();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableDifficulties()
	 */
	public List<Difficulty> getAvailableDifficulties() {
		return difficultyDao.getDifficulties();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableEthnicities()
	 */
	public List<Ethnicity> getAvailableEthnicities() {
		return ethnicityDao.getEthnicities();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailablePrepTimes()
	 */
	public List<PrepTime> getAvailablePrepTimes() {
		return prepTimeDao.getPrepTimes();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableUnitsForSystem(magoffin.matt.ieat.domain.System)
	 */
	public List<Unit> getAvailableUnitsForSystem(System sys) {
		if ( sys.getSystemId() == null ) {
			// return ALL units since we don't know the system
			return unitDao.getUnits();
		}
		return unitDao.getUnitsForSystem(sys);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getAvailableIngredients()
	 */
	public List<Ingredient> getAvailableIngredients() {
		return ingredientDao.getIngredients();
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#storeRecipe(magoffin.matt.ieat.domain.Recipe, magoffin.matt.ieat.biz.BizContext)
	 */
	@SuppressWarnings("unchecked")
	public Recipe storeRecipe(Recipe r, BizContext context) {
		BindException errors = new BindException(r,"recipe");
		recipeValidator.validate(r, errors); 
		if ( errors.hasErrors() ) { 
			throw new RuntimeException(errors); 
		}
		
		// remove dummy units and verify no circular ingredients
		List<RecipeIngredient> l = r.getIngredient();
		int i = 0;
		for ( Iterator<RecipeIngredient> itr = l.iterator(); itr.hasNext(); i++ ) {
			RecipeIngredient ri = itr.next();
			if ( ri.getUnit() != null && ri.getUnit().getUnitId() != null 
					&& ri.getUnit().getUnitId().intValue() < 1 ) {
				// remove any dummy unit with ID less than 1
				ri.setUnit(null);
			}
			if ( r.getRecipeId() != null && ri.getRecipe() != null && ri.getRecipe().getRecipeId() != null ) {
				Long result = verifyNoCircularRecipeIngredients(r.getRecipeId(),ri.getRecipe().getRecipeId());
				if ( result != null ) {
					if ( result.equals(r.getRecipeId()) ) {
						errors.rejectValue("ingredient["+i+"].recipe.recipeId",
								"edit-ingredient.self.recipe.ingredient",null,null);
					} else {
						Recipe otherRecipe = getRecipeById(result);
						errors.rejectValue("ingredient["+i+"].recipe.recipeId",
								"edit-ingredient.circular.recipe.ingredient",
								new Object[]{otherRecipe.getName()},null);
					}
					throw new EatValidationException(errors);
				}
			}
		}
		
		if ( r.getRecipeId() == null ) {
			if ( r.getCreatedDate() == null ) {
				r.setCreatedDate(Calendar.getInstance());
			}
			if ( context.getActingUser() != null ) {
				r.setOwner(context.getActingUser());
			}
			Long id = recipeDao.store(r);
			return recipeDao.get(id);
		}
		r.setModifiedDate(Calendar.getInstance());
		recipeDao.store(r);
		return recipeDao.get(r.getRecipeId());
	}
	
	
	/**
	 * Verify a recipe ingredient does not contain any circular references.
	 * 
	 * @param recipeId the recipe ID
	 * @param ingredientRecipeId the ID of the recipe ingredient
	 * @return the illegal recipe ingredient ID, or <em>null</em> if none found
	 */
	@SuppressWarnings("unchecked")
	private Long verifyNoCircularRecipeIngredients(Long recipeId, Long ingredientRecipeId) {
		if ( recipeId.equals(ingredientRecipeId) ) {
			return recipeId;
		}
		Recipe r = getRecipeById(ingredientRecipeId);
		for ( Iterator<RecipeIngredient> itr = r.getIngredient().iterator(); itr.hasNext(); ) {
			RecipeIngredient ri = itr.next();
			if ( ri.getRecipe() != null && ri.getRecipe().getRecipeId() != null ) {
				if ( recipeId.equals(ri.getRecipe().getRecipeId())) {
					return ingredientRecipeId;
				}
				Long result = verifyNoCircularRecipeIngredients(recipeId,ri.getRecipe().getRecipeId());
				if ( result != null ) {
					return result;
				}
			}
		}
		return null;
	}

	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#storeIngredient(magoffin.matt.ieat.domain.Ingredient, magoffin.matt.ieat.biz.BizContext)
	 */
	public Ingredient storeIngredient(Ingredient ingredient, BizContext context) {
		Integer id = ingredientDao.store(ingredient);
		return ingredientDao.get(id);
	}
	
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#removeIngredient(java.lang.Integer)
	 */
	public void removeIngredient(Integer ingredientId) {
		ingredientDao.delete(ingredientDao.get(ingredientId));
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#removeRecipe(Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	public void removeRecipe(Long recipeId, BizContext context) {
		Recipe r = recipeDao.get(recipeId);

		// verify recipe not used by any other recipes first...
		List<Recipe> usedIn = recipeDao.findRecipesUsingRecipeAsIngredient(recipeId);
		if ( usedIn.size() > 0 ) {
			BindException errors = new BindException(r,"recipe");
			StringBuilder buf = new StringBuilder();
			for ( Recipe usedInRecipe : usedIn ) {
				if ( buf.length() > 0 ) {
					buf.append(", ");
				}
				buf.append(usedInRecipe.getName());
			}
			errors.reject("delete-recipe.error.usedin.others", new Object[] {buf.toString()}, 
					"The recipe cannot be deleted because it is used as an ingredient in these recipes: "
					+buf.toString());
			throw new EatValidationException(errors);
		}
		
		recipeDao.delete(r);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#storeMeal(magoffin.matt.ieat.domain.Meal, magoffin.matt.ieat.biz.BizContext)
	 */
	public Meal storeMeal(Meal meal, BizContext context) {
		if ( meal.getOwner() == null ) {
			if ( context.getActingUser() == null ) {
				throw new IllegalActionException("No owner associated with menu.");
			}
			meal.setOwner(context.getActingUser());
		}
		Long id = mealDao.store(meal);
		return mealDao.get(id);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getMealsForUser(java.lang.Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	public List<Meal> getMealsForUser(Integer userId, BizContext context) {
		return mealDao.getMealsForOwner(userId);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#getMealById(java.lang.Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	public Meal getMealById(Long mealId, BizContext context) {
		// TODO meal security
		return mealDao.get(mealId);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#removeMeal(java.lang.Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	public void removeMeal(Long mealId, BizContext conetxt) {
		// TODO meal security
		mealDao.delete(mealDao.get(mealId));
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#addRecipeToMeal(java.lang.Integer, magoffin.matt.ieat.domain.MealRecipe, magoffin.matt.ieat.biz.BizContext)
	 */
	@SuppressWarnings("unchecked")
	public Meal addRecipeToMeal(Long mealId, MealRecipe mr,
			BizContext context) 
	{
		Meal m = getMealById(mealId,context);
		m.getRecipe().add(mr);
		m = storeMeal(m,context);
		return m;
	}
	
	private static final class SortShoppingList implements Comparator<RecipeIngredient> {
		public int compare(RecipeIngredient o1, RecipeIngredient o2) {
			if ( o1 == null && o2 == null ) return 0;
			if ( o2 == null ) return -1;
			if ( o1 == null ) return 1;
			Ingredient i1 = o1.getIngredient();
			Ingredient i2 = o2.getIngredient();
			int cmp = i1.getName().compareToIgnoreCase(i2.getName());
			if ( cmp != 0 ) {
				return cmp;
			}
			Unit u1 = o1.getUnit();
			Unit u2 = o2.getUnit();
			if ( u1 == null && u2 == null ) return 0;
			if ( u2 == null ) return -1;
			if ( u1 == null ) return 1;
			return u1.getValue().compareToIgnoreCase(u2.getValue());
		}
	}
	
	private static final Comparator<RecipeIngredient> SORT_SHOPPING_LIST = new SortShoppingList();
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#generateShoppingList(java.lang.Integer, java.lang.Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	@SuppressWarnings("unchecked")
	public Recipe generateShoppingList(Long mealId, Integer systemId,
			BizContext context) 
	{
		Meal meal = getMealById(mealId,context);
		Map combinedIngredients = new HashMap();
		
		// convert each recipe to desired system, and add converted ingredients
		// to shopping list so we can combine like ingredients
		for ( Iterator itr = meal.getRecipe().iterator(); itr.hasNext(); ) {
			MealRecipe mr = (MealRecipe)itr.next();
			if ( mr == null ) continue;
			for ( Iterator ingredItr = mr.getRecipe().getIngredient().iterator(); ingredItr.hasNext(); ) {
				RecipeIngredient ri = (RecipeIngredient)ingredItr.next();
				// if ingredient is recipe, get that recipe and add all ingredients
				generateNestedRecipeShoppingList(ri,systemId,combinedIngredients,mr.getQuantity());
			}
		}
		
		// sort ingredients
		SortedBag bag = new TreeBag(SORT_SHOPPING_LIST);
		bag.addAll(combinedIngredients.values());
		
		Recipe shopList = domainObjectFactory.getRecipeInstance();
		shopList.getIngredient().addAll(bag);
		System system = systemDao.get(systemId);
		shopList.setSystem(system);
		return shopList;
	}
	
	@SuppressWarnings("unchecked")
	private void generateNestedRecipeShoppingList(RecipeIngredient ri, Integer systemId,
			Map<String, RecipeIngredient> combinedIngredients, double quantity) {
		if ( ri.getRecipe() != null ) {
			for ( Iterator<RecipeIngredient> itr = ri.getRecipe().getIngredient().iterator(); 
					itr.hasNext(); ) {
				RecipeIngredient nestedRi = itr.next();
				generateNestedRecipeShoppingList(nestedRi,systemId,combinedIngredients,quantity*ri.getQuantity());
			}
		} else {
			convert(ri,systemId,combinedIngredients,quantity,true);
		}
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#convertRecipeSystem(java.lang.Integer, java.lang.Integer)
	 */
	@SuppressWarnings("unchecked")
	public Recipe convertRecipeSystem(Long recipeId, Integer systemId) {
		Recipe original = getRecipeById(recipeId);
		if ( original.getSystem().getSystemId().equals(systemId) ) {
			// nothing to do
			return original;
		}
		
		Recipe converted = (Recipe)domainObjectFactory.clone(original);
		
		Map<String, RecipeIngredient> convertedIngredients 
			= new LinkedHashMap<String, RecipeIngredient>();
		for ( Iterator itr = original.getIngredient().iterator(); itr.hasNext(); ) {
			RecipeIngredient ri = (RecipeIngredient)itr.next();
			convert(ri,systemId,convertedIngredients,1.0,false);
		}
		
		converted.getIngredient().clear();
		converted.getIngredient().addAll(convertedIngredients.values());
		
		System system = systemDao.get(systemId);
		converted.setSystem(system);
		
		return converted;
	}
	
	private static final String STANDARDIZED_CONVERSION_KEY = "_SDZ_";
	
	/**
	 * Convert a recipe ingredient to a specified system.
	 * 
	 * <p>Note the RecipeIngredient objects populated in <var>ingredientMap</var>
	 * will not necessarily have persistent keys associated with them.</p>
	 * 
	 * @param ri the RecipeIngredient to convert
	 * @param systemId the System ID to convert to
	 * @param ingredientMap a Map of converted RecipeIngredient objects
	 * @param quantity the quantity to multiply the ingredient units by
	 * @param consolidate if <em>true</em> then combine similar ingredients if 
	 * possible
	 */
	private void convert(RecipeIngredient ri, Integer systemId, 
			Map<String, RecipeIngredient> ingredientMap, 
			double quantity, boolean consolidate) 
	{
		String mapKey = ri.getRecipe() != null 
			? "R"+ri.getRecipe().getRecipeId()
			: "I"+ri.getIngredient().getIngredientId();
		double convFactor = 1;

		if ( !consolidate ) {
			mapKey += java.lang.System.currentTimeMillis();
		}

		Unit unit = ri.getUnit();
		if ( unit != null ) {
			if ( METRIC_SYSTEM_ID.equals(systemId) && unit.getToMetric() > 0) {
				mapKey = mapKey+STANDARDIZED_CONVERSION_KEY+unit.isMass();
				convFactor = unit.getToMetric();
			} else if ( US_SYSTEM_ID.equals(systemId) && unit.getToUS() > 0) {
				mapKey = mapKey+STANDARDIZED_CONVERSION_KEY+unit.isMass();
				convFactor = unit.getToUS();
			} else {
				mapKey = mapKey+unit.getUnitId();
			}
		}
		
		RecipeIngredient combo = null;
		if ( ingredientMap.containsKey(mapKey) ) {
			combo = ingredientMap.get(mapKey);
		} else {
			combo = domainObjectFactory.getRecipeIngredientInstance();
			combo.setQuantity(0);
			Ingredient i = domainObjectFactory.getIngredientInstance();
			i.setName( ri.getRecipe() != null 
					? ri.getRecipe().getName()
					: ri.getIngredient().getName());
			combo.setIngredient(i);
			if ( unit != null ) {
				Unit newUnit = domainObjectFactory.getUnitInstance();
				BeanUtils.copyProperties(unit,newUnit);
				combo.setUnit(newUnit);
			}
			ingredientMap.put(mapKey,combo);
		}
		
		double convertedQuantity = ri.getQuantity() * convFactor * quantity;
		combo.setQuantity(combo.getQuantity()+convertedQuantity);
	}
	
	private RecipeIngredient alter(RecipeIngredient ri, float size) {
		RecipeIngredient altered = domainObjectFactory.getRecipeIngredientInstance();
		BeanUtils.copyProperties(ri,altered);
		double newQuantity = ri.getQuantity() * size;
		altered.setQuantity(newQuantity);
		return altered;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#alterRecipeSize(java.lang.Integer, float)
	 */
	@SuppressWarnings("unchecked")
	public Recipe alterRecipeSize(Long recipeId, float size) {
		Recipe original = getRecipeById(recipeId);
		if ( size <= 0 || size == 1 ) {
			// nothing to do
			return original;
		}
		
		Recipe altered = (Recipe)domainObjectFactory.clone(original);
		
		// adjust servings (rounded down)
		Integer servings = altered.getServingSize();
		int newServings = (int)Math.floor(
				(servings == null ? 1.0 : servings.doubleValue()) * size);
		altered.setServingSize(newServings);

		altered.getIngredient().clear();
		
		for ( Iterator itr = original.getIngredient().iterator(); itr.hasNext(); ) {
			RecipeIngredient ri = (RecipeIngredient)itr.next();
			ri = alter(ri,size);
			altered.getIngredient().add(ri);
		}
		
		return altered;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#storeRecipeRating(java.lang.Integer, short, magoffin.matt.ieat.biz.BizContext)
	 */
	@SuppressWarnings("unchecked")
	public RecipeRating storeRecipeRating(Long recipeId, short rating,
			BizContext context) {
		if ( context.getActingUser() == null ) {
			throw new IllegalActionException("No owner associated with rating.");
		}
		Integer userId = context.getActingUser().getUserId();
		Recipe r = getRecipeById(recipeId);
		
		// see if user's rating already exists, and if so update that
		for ( Iterator itr = r.getRating().iterator(); itr.hasNext(); ) {
			RecipeRating rr = (RecipeRating)itr.next();
			if ( userId.equals(rr.getUserId()) ) {
				if ( rating != rr.getRating() ) {
					rr.setRating(rating);
					rr.setCreatedDate(Calendar.getInstance());
					storeRecipe(r,context);
				}
				return rr;
			}
		}
		
		// user hasn't rated this recipe, so add new rating now
		RecipeRating rr = domainObjectFactory.getRecipeRatingInstance();
		rr.setUserId(userId);
		rr.setRating(rating);
		rr.setCreatedDate(Calendar.getInstance());
		
		r.getRating().add(rr);
		storeRecipe(r,context);
		return rr;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#addCommentToRecipe(java.lang.Integer, java.lang.String, magoffin.matt.ieat.biz.BizContext)
	 */
	@SuppressWarnings("unchecked")
	public RecipeComment addCommentToRecipe(Long recipeId, String comment,
			BizContext context) {
		if ( context.getActingUser() == null ) {
			throw new IllegalActionException("No owner associated with comment.");
		}
		comment = StringUtil.trimToNull(comment);
		if ( comment == null ) {
			return null;
		}
		
		Integer userId = context.getActingUser().getUserId();
		Recipe r = getRecipeById(recipeId);
		
		// add new comment to recipe
		RecipeComment rc = domainObjectFactory.getRecipeCommentInstance();
		rc.setUserId(userId);
		rc.setValue(comment);
		rc.setCreatedDate(Calendar.getInstance());
		
		r.getComment().add(rc);
		storeRecipe(r,context);
		return rc;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#removeRecipeImage(java.lang.Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	public Recipe removeRecipeImage(Long recipeId, BizContext context) {
		Recipe r = getRecipeById(recipeId);
		if ( r.getImage() != null ) {
			File f = getImageFile(r);
			if ( f != null ) {
				if ( !f.delete() ) {
					log.warn("Unable to delete image file [" +f.getAbsolutePath() +"]");
				}
			}
			r.setImage(null);
			r = storeRecipe(r,context);
		}
		return r;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.RecipeBiz#storeRecipeImage(java.lang.Integer, magoffin.matt.ieat.util.TemporaryFile, magoffin.matt.ieat.biz.BizContext)
	 */
	public Recipe storeRecipeImage(Long recipeId, TemporaryFile image,
			BizContext context) {
		Recipe r = getRecipeById(recipeId);
		
		// get extension and verify supported
		String ext = StringUtil.substringAfter(image.getName(),'.');
		if ( ext != null ) {
			ext = ext.toLowerCase();
		}
		if ( !imageSupportedExtensions.contains(ext) ) {
			Errors errors = new BindException(r,"recipe");
			errors.reject("recipe.image.unsupported", new String[] {ext},
					"File type [" +ext +"] is not supported for recipe images.");
			throw new EatValidationException(errors);
		}
		
		String imageFileName = recipeId +"." +ext;
		
		File oldImageFile = getImageFile(r);
		
		File imageFile = new File(imageBaseDir,imageFileName);
		if ( log.isDebugEnabled() ) {
			log.debug("Writing image file [" +imageFile.getAbsolutePath() +"]");
		}
		try {
			FileCopyUtils.copy(image.getInputStream(),
					new BufferedOutputStream(new FileOutputStream(imageFile)));
			r.setImage(imageFileName);
		} catch (IOException e) {
			log.error("Unable to write image file [" +imageFile.getAbsolutePath() +"]");
			throw new RuntimeException("Unable to write image file",e);
		}
		
		r = storeRecipe(r,context);
		
		if ( oldImageFile != null && !imageFileName.equals(oldImageFile.getName()) ) {
			// delete old file
			if ( !oldImageFile.delete() ) {
				log.warn("Unable to delete image file [" +oldImageFile.getAbsolutePath() +"]");
			}
		}
		
		return r;
	}
	
	private File getImageFile(Recipe r) {
		if ( r.getImage() == null ) {
			return null;
		}
		File f = new File(imageBaseDir,r.getImage());
		if ( f.exists() ) {
			return f;
		}
		return null;
	}
	
	/**
	 * @return the baseDao
	 */
	public BaseDao getBaseDao() {
		return baseDao;
	}
	
	/**
	 * @param baseDao the baseDao to set
	 */
	public void setBaseDao(BaseDao baseDao) {
		this.baseDao = baseDao;
	}
	
	/**
	 * @return the courseDao
	 */
	public CourseDao getCourseDao() {
		return courseDao;
	}
	
	/**
	 * @param courseDao the courseDao to set
	 */
	public void setCourseDao(CourseDao courseDao) {
		this.courseDao = courseDao;
	}
	
	/**
	 * @return the difficultyDao
	 */
	public DifficultyDao getDifficultyDao() {
		return difficultyDao;
	}
	
	/**
	 * @param difficultyDao the difficultyDao to set
	 */
	public void setDifficultyDao(DifficultyDao difficultyDao) {
		this.difficultyDao = difficultyDao;
	}
	
	/**
	 * @return the domainObjectFactory
	 */
	public DomainObjectFactory getDomainObjectFactory() {
		return domainObjectFactory;
	}
	
	/**
	 * @param domainObjectFactory the domainObjectFactory to set
	 */
	public void setDomainObjectFactory(DomainObjectFactory domainObjectFactory) {
		this.domainObjectFactory = domainObjectFactory;
	}
	
	/**
	 * @return the ethnicityDao
	 */
	public EthnicityDao getEthnicityDao() {
		return ethnicityDao;
	}
	
	/**
	 * @param ethnicityDao the ethnicityDao to set
	 */
	public void setEthnicityDao(EthnicityDao ethnicityDao) {
		this.ethnicityDao = ethnicityDao;
	}
	
	/**
	 * @return the imageBasePath
	 */
	public String getImageBasePath() {
		return imageBasePath;
	}
	
	/**
	 * @param imageBasePath the imageBasePath to set
	 */
	public void setImageBasePath(String imageBasePath) {
		this.imageBasePath = imageBasePath;
	}
	
	/**
	 * @return the imageSupportedExtensions
	 */
	public Set<String> getImageSupportedExtensions() {
		return imageSupportedExtensions;
	}
	
	/**
	 * @param imageSupportedExtensions the imageSupportedExtensions to set
	 */
	public void setImageSupportedExtensions(Set<String> imageSupportedExtensions) {
		this.imageSupportedExtensions = imageSupportedExtensions;
	}
	
	/**
	 * @return the ingredientDao
	 */
	public IngredientDao getIngredientDao() {
		return ingredientDao;
	}
	
	/**
	 * @param ingredientDao the ingredientDao to set
	 */
	public void setIngredientDao(IngredientDao ingredientDao) {
		this.ingredientDao = ingredientDao;
	}
	
	/**
	 * @return the mealDao
	 */
	public MealDao getMealDao() {
		return mealDao;
	}
	
	/**
	 * @param mealDao the mealDao to set
	 */
	public void setMealDao(MealDao mealDao) {
		this.mealDao = mealDao;
	}
	
	/**
	 * @return the prepTimeDao
	 */
	public PrepTimeDao getPrepTimeDao() {
		return prepTimeDao;
	}
	
	/**
	 * @param prepTimeDao the prepTimeDao to set
	 */
	public void setPrepTimeDao(PrepTimeDao prepTimeDao) {
		this.prepTimeDao = prepTimeDao;
	}
	
	/**
	 * @return the recipeDao
	 */
	public RecipeDao getRecipeDao() {
		return recipeDao;
	}
	
	/**
	 * @param recipeDao the recipeDao to set
	 */
	public void setRecipeDao(RecipeDao recipeDao) {
		this.recipeDao = recipeDao;
	}
	
	/**
	 * @return the recipeValidator
	 */
	public Validator getRecipeValidator() {
		return recipeValidator;
	}
	
	/**
	 * @param recipeValidator the recipeValidator to set
	 */
	public void setRecipeValidator(Validator recipeValidator) {
		this.recipeValidator = recipeValidator;
	}
	
	/**
	 * @return the systemDao
	 */
	public SystemDao getSystemDao() {
		return systemDao;
	}
	
	/**
	 * @param systemDao the systemDao to set
	 */
	public void setSystemDao(SystemDao systemDao) {
		this.systemDao = systemDao;
	}
	
	/**
	 * @return the unitDao
	 */
	public UnitDao getUnitDao() {
		return unitDao;
	}
	
	/**
	 * @param unitDao the unitDao to set
	 */
	public void setUnitDao(UnitDao unitDao) {
		this.unitDao = unitDao;
	}
	
}
