# This file is part of Pantry, command-line nutrient analysis.
# 
# Version 23, released Friday, November 09, 2007.
# 
# Copyright 2007 Omari Norman.
# 
# 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.
# 
# Pantry's "built-in" XML validator is a modified version of
# xmlproc.  xmlproc is Copyright 1998-2000 by Lars Marius
# Garshol, Oslo, Norway.
# 
# All Rights Reserved
# 
# Permission to use, copy, modify, and distribute this
# software and its documentation for any purpose and without
# fee is hereby granted, provided that the above copyright
# notice appear in all copies and that modified copies are
# clearly marked as such.
# 
# LARS MARIUS GARSHOL DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL LARS MARIUS
# GARSHOL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

""" Food objects, the most basic Pantry object.

Foods are the most basic object in Pantry. Cabinets hold multiple
Food objects. There are two kinds of foods: Food instances, and
Recipe instances. A Food has its own nutrient information. A Recipe
gets its nutrient information from its ingredient Foods (which may be
Food instances or Recipe instances.)

The first place to look for documentation is the docstrings for the
Recipe and Food objects.
"""

from __future__ import division
import UserDict

class _FoodBase(object):
    """ Base object for foods.

    Both regular Foods and Recipes are derived from this base class. 

    Instances have two attributes:
        traits - dict of traits
        arbUnits - dict of the food's arbitrary units
   
    """
    
    def __init__(self, traits, arbUnits):
        self.arbUnits = arbUnits
        self.traits = traits

    def __str__(self):
        import pprint
        rep = ('Hash: %s\nTraits:\n%s\narbUnits:\n%s\nNuts:\n%s\n' % 
        (repr(self.hash),
            pprint.pformat(self.traits),
            pprint.pformat(self.arbUnits),
            pprint.pformat(self.nuts)))
        return rep

    def _getGrams(self):
        import uniconv
        qtyNo = uniconv.convertUni(self.traits[TRAITSBYNAME['qty']])
        gramWeight = self.units[self.traits[TRAITSBYNAME['unit']]]
        return qtyNo * gramWeight

    grams = property(fget = _getGrams,
            doc = "Weight of this food, considering quantity and units.")

    def _getQtyMinusRefuse(self):
        """ Gets the quantity of this food, reduced by its percent refuse. """
        import uniconv

        pctRefuse = uniconv.convertUni(self.traits[TRAITSBYNAME['pctRefuse']])
        oldQty = uniconv.convertUni(self.traits[TRAITSBYNAME['qty']])
        newQty = oldQty - pctRefuse / 100 * oldQty
        return str(newQty)

    qtyMinusRefuse = property(fget = _getQtyMinusRefuse,
            doc = """ Quantity of food, reduced by percent refuse. """)

    def _getHash(self):
        """ Hash of this food.

        The hash is only of the food's traits; nuts and arbUnits are
        not considered when calculating the hash. """
        
        # hashlib new in python-2.5
        try:
            import hashlib
            hasher = hashlib.sha1
        except ImportError:
            import sha 
            hasher = sha.sha
        traits = self.traits

        fingerPrint = ''.join([traits[traitNo].encode('utf-8')
            for traitNo in sorted(traits.keys())])
        return hasher(fingerPrint).digest()

    hash = property(fget = _getHash, doc = _getHash.__doc__)

    def getQtyByNut(self, nutNo, nutAmt):
        """ Return qty needed for amount of nutNo to equal nutAmt.

        nutAmt is a float or int. """

        import uniconv

        nutValue = self.nuts[nutNo]

        foodQty = ((uniconv.convertUni(self.traits[TRAITSBYNAME['qty']]) * 
            nutAmt) / nutValue)
        return str(round(foodQty, 2))

    def _mutant(self):
        """ Returns copy of food, with Comment field changed.

        Handy for adding foods to a cabinet without adding dupes. 
        Returns 2-tuple, with first element being the mutated food,
        and the second element being an integer with the copy number. """

        import re
        newFood = self.copy()
        regex = re.compile(r'\(Copy ([0-9]+)\) (.*)')
        oldComment = newFood.traits.get(TRAITSBYNAME['comment'], '')
        match = regex.match(oldComment)
        if match:
            mutex = int(match.group(1)) + 1
            newFood.traits[TRAITSBYNAME['comment']] = ('(Copy %u) %s' 
                    % (mutex, match.group(2)))
        else:
            mutex = 1
            newFood.traits[TRAITSBYNAME['comment']] = ('(Copy 1) %s' 
                    % oldComment)
        return (newFood, mutex)

    mutant = property(fget=_mutant, doc=_mutant.__doc__)

class Recipe(_FoodBase):
    """ Defines a recipe. """
    
    def __init__(self, traits, arbUnits, ingredients): 
        """ Set up a new recipe. 

        Arguments:
            traits - {traitNo:u'traitValue'}
            arbUnits - {'arbUnitName':gramWeight}
            ingredients - list of foods that are ingredients
        
        """
        self.ingredients = ingredients
        _FoodBase.__init__(self, traits, arbUnits)

    def __str__(self):
        rep = ('%s\nIngredients:%s\n' % (_FoodBase.__str__(self),
                '\n'.join([str(ingredient) for ingredient in ingredients])))
        return rep

    def _getTotalGrams(self):
        """ Total grams yielded by one batch of this recipe.

        Not to be confused with grams attribute, which is the total
        grams of a single portion of the recipe, as determined by the
        qty and unit attributes. """
        from uniconv import convertUni

        try:
            result = convertUni(self.traits[TRAITSBYNAME['yieldGrams']])
        except KeyError:
            result = sum([food.grams for food in self.ingredients])
        return result

    totalGrams = property(fget=_getTotalGrams, doc=_getTotalGrams.__doc__)

    def _getNuts(self):
        """ Mapping instance of nutrients in this food.

        Keys are (nutName, nutUnits); values are nutrient amounts. """

        grams = self.grams
        totalGrams = self.totalGrams
        return dict([(nutId, nutValue / totalGrams * grams)
            for nutId, nutValue in nutSum(self.ingredients).items()])

    nuts = property(fget=_getNuts, doc=_getNuts.__doc__)

    def _getData(self):
        """ Dict with data that may be used to reconstruct this recipe.

        e.g. to copy the recipe, Recipe(**recipe.data). Every value
        in the dict has only Python primatives, making it easy to
        pickle.
        """
        return {'traits':self.traits.copy(),
                'arbUnits':self.arbUnits.copy(),
                'ingredients':[food.copy() for food in self.ingredients]}

    data = property(fget = _getData, doc = _getData.__doc__)

    def _getUnits(self):
        """ Units available for this food.

        Absolute units "mask" arbitrary units--that is,
        if there is an absolute unit and an arbitrary unit
        with the same description, this attribute contains only 
        the absolute unit. 
        
        Units for foods also include a 'serving' unit if
        the 'servings' rTrait is set."""
        import uniconv
        result = dict(self.arbUnits.items() + ABSOLUTES.items())
        try:
            servings = uniconv.convertUni(self.traits[TRAITSBYNAME['servings']])
        except KeyError:
            pass
        else:
            servingWeight = self.totalGrams / servings
            result['serving'] = servingWeight
        return result

    units = property(fget = _getUnits, doc = _getUnits.__doc__ )

    def copy(self):
        """ Return a deep copy of this recipe. """
        return Recipe(**self.data)

class Food(_FoodBase):
    """ Defines a food. """

    def __init__(self, traits, arbUnits, nutsPer100g):
        """ Set up a new food. 

        traits - {traitNo:u'traitValue'}
        arbUnits - {u'arbUnitName':gramWeight}
        nutsPer100g - {nutNo:nutValuePer100g}

        """

        _FoodBase.__init__(self, traits, arbUnits)
        self.nutsPer100g = nutsPer100g

    def _getData(self):
        """ Dict with data that may be used to reconstruct this food.

        e.g. to copy the food, you can do Food(**food.data).
        """

        return {'traits':self.traits.copy(),
                'arbUnits':self.arbUnits.copy(), 
                'nutsPer100g':self.nutsPer100g.copy()}

    data = property(fget = _getData, doc = _getData.__doc__)

    def copy(self):
        """ Return a deep copy of this food. """
        return Food(**self.data)

    def _getUnits(self):
        """ Units available for this food.

        Absolute units "mask" arbitrary units--that is,
        if there is an absolute unit and an arbitrary unit
        with the same description, this attribute contains only 
        the absolute unit. """
        return dict(self.arbUnits.items() + ABSOLUTES.items())

    units = property(fget = _getUnits, doc = _getUnits.__doc__ )

    def _getNuts(self):
        """ Mapping of nutrients in this food. 
        
        Adjusted by qty and gram weight. """
        nutsPer100g = self.nutsPer100g
        grams = self.grams
       
        return dict([(nutNo, per100g * grams / 100)
            for nutNo, per100g in nutsPer100g.items()])

    nuts = property(fget = _getNuts, doc = _getNuts.__doc__)

class CmpFoods(object):
    """ Compare two foods for sorting purposes. """

    def __init__(self, traits, sortOrders):
        """ Set up a food comparison object.

        traits is a sequence of tuples. The first value of each
        tuple is a trait number, as found in
        foods.TRAITSBYNO.keys(). The second value is either
        ASCENDING or DESCENDING.  Thus, to sort by name in ascending
        order and by group in descending order, pass
        ((TRAITSBYNAME['name'], ASCENDING),
        (TRAITSBYNAME['group'], DESCENDING)) for traits.

        sortOrders is a mapping. The keys are the numbers of the
        traits; the values are tuples specifying what order the
        traits are to be sorted in. For example, to specify that the
        meal trait is to be sorted in the order 'Breakfast, Lunch,
        Dinner', pass {foods.TRAITSBYNAME['meal']:('Breakfast',
        'Lunch', 'Dinner')} for sortOrders. If a sort order is
        specified for a particular trait, and one food has a trait
        that is in the sort order and another food does not have a
        trait that is in the sort order, the food whose trait is in
        the sort order is sorted superior to the one whose trait is
        not in the sort order.

        """
        self.traits = traits
        self.sortOrders = sortOrders

    def __call__(self, x, y):
        """ Compare foods x and y.

        Returns 1 if x>y, 0 if x==y, -1 if x<y. 

        """

        for traitNo, order in self.traits:
            xTrait = x.traits.get(traitNo, '')
            yTrait = y.traits.get(traitNo, '')
            if traitNo in self.sortOrders:
                sortOrder = self.sortOrders[traitNo]
                xIn = xTrait in sortOrder
                yIn = yTrait in sortOrder
                if xIn and yIn:
                    result = cmp(sortOrder.index(xTrait),
                            sortOrder.index(yTrait))
                elif not xIn and not yIn:
                    result = cmp(xTrait, yTrait)
                else:
                    result = cmp(xIn, yIn)
            else:
                result = cmp(xTrait, yTrait)
            
            if order == DESCENDING:
                result = -result
            if result != 0:
                return result
        return 0

def nutSum(foods):
    """ Sums nutrients.

    foods is a tuple of instances having a nuts attribute. nutSum
    sums these and returns a single mapping with the sum. """

    # it is possible to do this with list comprehensions,
    # but it is impossible to read the code that way
    # and it is unknown whether list comprehensions actually 
    # produce a performance boost here
    result = {}
    for food in foods:
        for nutID, nutValue in food.nuts.items():
            try:
                result[nutID] += nutValue
            except KeyError:
                result[nutID] = nutValue
    return result

### CONSTANTS
ALLNUTSTUPLE = (
    (203, 'Protein', 'g', 50),
    (204, 'Total Fat', 'g', 65),
    (205, 'Total Carbohydrate', 'g', 300),
    (207, 'Ash', 'g', None),
    (208, 'Calories', 'kcal', None),
    (209, 'Starch', 'g', None),
    (210, 'Sucrose', 'g', None),
    (211, 'Glucose (dextrose)', 'g', None),
    (212, 'Fructose', 'g', None),
    (213, 'Lactose', 'g', None),
    (214, 'Maltose', 'g', None),
    (221, 'Alcohol, ethyl', 'g', None),
    (255, 'Water', 'g', None),
    (257, 'Adjusted Protein', 'g', None),
    (262, 'Caffeine', 'mg', None),
    (263, 'Theobromine', 'mg', None),
    (268, 'Energy', 'kj', None),
    (269, 'Sugars', 'g', None),
    (287, 'Galactose', 'g', None),
    (291, 'Dietary Fiber', 'g', 25),
    (301, 'Calcium', 'mg', 1000),
    (303, 'Iron', 'mg', 18),
    (304, 'Magnesium', 'mg', 400),
    (305, 'Phosphorus', 'mg', 1000),
    (306, 'Potassium', 'mg', 3500),
    (307, 'Sodium', 'mg', 2400),
    (309, 'Zinc', 'mg', 15),
    (312, 'Copper', 'mg', 2),
    (313, 'Fluoride', 'mcg', None),
    (315, 'Manganese', 'mg', None),
    (317, 'Selenium', 'mcg', None),
    (318, 'Vitamin A', 'IU', 5000),
    (319, 'Retinol', 'mcg', None),
    (320, 'Vitamin A, RAE', 'mcg_RAE', None),
    (321, 'Carotene, beta', 'mcg', None),
    (322, 'Carotene, alpha', 'mcg', None),
    (323, 'Vitamin E', 'mg', None),
    (324, 'Vitamin D', 'IU', 400),
    (334, 'Cryptoxanthin, beta', 'mcg', None),
    (337, 'Lycopene', 'mcg', None),
    (338, 'Lutein + zeaxanthin', 'mcg', None),
    (341, 'Tocopherol, beta', 'mg', None),
    (342, 'Tocopherol, gamma', 'mg', None),
    (343, 'Tocopherol, delta', 'mg', None),
    (401, 'Vitamin C', 'mg', 60),
    (404, 'Thiamin', 'mg', 1.5),
    (405, 'Riboflavin', 'mg', 1.7),
    (406, 'Niacin', 'mg', 20),
    (410, 'Pantothenic acid', 'mg', 10),
    (415, 'Vitamin B-6', 'mg', 2),
    (417, 'Folate, total', 'mcg', None),
    (418, 'Vitamin B-12', 'mcg', 6),
    (421, 'Choline, total', 'mg', None),
    (430, 'Vitamin K (phylloquinone)', 'mcg', None),
    (431, 'Folic acid', 'mcg', 400),
    (432, 'Folate, food', 'mcg', None),
    (435, 'Folate, DFE', 'mcg_DFE', None),
    (454, 'Betaine', 'mg', None),
    (501, 'Tryptophan', 'g', None),
    (502, 'Threonine', 'g', None),
    (503, 'Isoleucine', 'g', None),
    (504, 'Leucine', 'g', None),
    (505, 'Lysine', 'g', None),
    (506, 'Methionine', 'g', None),
    (507, 'Cystine', 'g', None),
    (508, 'Phenylalanine', 'g', None),
    (509, 'Tyrosine', 'g', None),
    (510, 'Valine', 'g', None),
    (511, 'Arginine', 'g', None),
    (512, 'Histidine', 'g', None),
    (513, 'Alanine', 'g', None),
    (514, 'Aspartic acid', 'g', None),
    (515, 'Glutamic acid', 'g', None),
    (516, 'Glycine', 'g', None),
    (517, 'Proline', 'g', None),
    (518, 'Serine', 'g', None),
    (521, 'Hydroxyproline', 'g', None),
    (573, 'Vitamin E, added', 'mg', None),
    (578, 'Vitamin B-12, added', 'mcg', None),
    (601, 'Cholesterol', 'mg', 300),
    (605, 'Trans Fat', 'g', None),
    (606, 'Saturated Fat', 'g', 20),
    (607, '4:0', 'g', None),
    (608, '6:0', 'g', None),
    (609, '8:0', 'g', None),
    (610, '10:0', 'g', None),
    (611, '12:0', 'g', None),
    (612, '14:0', 'g', None),
    (613, '16:0', 'g', None),
    (614, '18:0', 'g', None),
    (615, '20:0', 'g', None),
    (617, '18:1 undifferentiated', 'g', None),
    (618, '18:2 undifferentiated', 'g', None),
    (619, '18:3 undifferentiated', 'g', None),
    (620, '20:4 undifferentiated', 'g', None),
    (621, '22:6 n-3', 'g', None),
    (624, '22:0', 'g', None),
    (625, '14:1', 'g', None),
    (626, '16:1 undifferentiated', 'g', None),
    (627, '18:4', 'g', None),
    (628, '20:1', 'g', None),
    (629, '20:5 n-3', 'g', None),
    (630, '22:1 undifferentiated', 'g', None),
    (631, '22:5 n-3', 'g', None),
    (636, 'Phytosterols', 'mg', None),
    (638, 'Stigmasterol', 'mg', None),
    (639, 'Campesterol', 'mg', None),
    (641, 'Beta-sitosterol', 'mg', None),
    (645, 'Monounsaturated Fat', 'g', None),
    (646, 'Polyunsaturated Fat', 'g', None),
    (652, '15:0', 'g', None),
    (653, '17:0', 'g', None),
    (654, '24:0', 'g', None),
    (662, '16:1 t', 'g', None),
    (663, '18:1 t', 'g', None),
    (664, '22:1 t', 'g', None),
    (665, '18:2 t not further defined', 'g', None),
    (666, '18:2 i', 'g', None),
    (669, '18:2 t,t', 'g', None),
    (670, '18:2 CLAs', 'g', None),
    (671, '24:1 c', 'g', None),
    (672, '20:2 n-6 c,c', 'g', None),
    (673, '16:1 c', 'g', None),
    (674, '18:1 c', 'g', None),
    (675, '18:2 n-6 c,c', 'g', None),
    (676, '22:1 c', 'g', None),
    (685, '18:3 n-6 c,c,c', 'g', None),
    (687, '17:1', 'g', None),
    (689, '20:3 undifferentiated', 'g', None),
    (693, 'Fatty acids, total trans-monoenoic', 'g', None),
    (695, 'Fatty acids, total trans-polyenoic', 'g', None),
    (696, '13:0', 'g', None),
    (697, '15:1', 'g', None),
    (851, '18:3 n-3 c,c,c', 'g', None),
    (852, '20:3 n-3', 'g', None),
    (853, '20:3 n-6', 'g', None),
    (855, '20:4 n-6', 'g', None),
    (856, '18:3i', 'g', None),
    (857, '21:5', 'g', None),
    (858, '22:4', 'g', None)
)

ALLNUTS = [ {'no':_no, 'name':_name, 'units':_units, 'dv':_dv}
        for _no, _name, _units, _dv in ALLNUTSTUPLE ]
NUTSBYNO = dict([(_nutdict['no'], _nutdict) for _nutdict in ALLNUTS])
NUTSBYNAME = dict([(_nutdict['name'], _nutdict) for _nutdict in ALLNUTS])

IGNORECASE = 1
EXACTMATCH = 2
ASCENDING = 4
DESCENDING = 8

TRAITNAMES = ('name', 'group', 'refDesc', 'pctRefuse',
        'unit', 'date', 'meal', 'qty', 'comment', 'order', 'origFile',
        'yieldGrams', 'yieldDesc', 'servings', 'directions')
FLOATTRAITS = ('qty', 'pctRefuse', 'yieldGrams', 'servings')
TRAITSBYNO = dict(enumerate(TRAITNAMES))
TRAITSBYNAME = dict(zip(TRAITNAMES, range(len(TRAITNAMES))))
ABSOLUTES = { u'g':1.0, u'oz':28.35, u'lb':453.59 }
