# 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.

from __future__ import division
from pantry.lib import foods

class ConfigVars:
    """ Holds configuration variables. """

    def __init__(self, filename = None):
        """ Set up configVars instance.

        If filename is None, looks at PANTRYRC environment variable
        to get location. If that environment variable is empty,
        looks at $HOME/.pantryrc.xml.
        
        If filename is passed in, or if PANTRYRC environment variable
        is set, the ConfigVars raises PantryRCNotFoundError if
        appropriate. Otherwise, ConfigVars returns default config
        variables. If a PantryRC is found but is invalid, 
        ConfigVars always raises InvalidPantryRCError, regardless
        of whether filename was passed in or PANTRYRC environment
        variable was set. filename, if passed, takes precedence over value
        of PANTRYRC environment variable."""
        
        import os
        import errno
        import xml.sax
        from pantry.lib import xmlCheck
        from pantry.lib import uniconv
        from pantry.lib import foods

        for key, value in DEFAULTS.items():
            setattr(self, key, value)

        acceptBlankFile = True
        toValidate = os.path.expanduser('~/.pantryrc.xml')
        env = os.getenv('PANTRYRC', None)
        if env:
            toValidate = env
            acceptBlankFile = False
        if filename:
            toValidate = filename
            acceptBlankFile = False

        try:
            fh = open(toValidate, 'r')
        except IOError, e:
            if e.errno == errno.ENOENT:
                if acceptBlankFile:
                    self.configFilename = None
                    return
                else:
                    raise PantryRCNotFoundError(toValidate)

        data = fh.read()
        try:
            xmlCheck.xmlCheck(data, DTD)
        except xmlCheck.BadXMLError, e:
            raise InvalidPantryRCError(toValidate, e)
        else:
            self.configFilename = toValidate

        class ConfigHandler(xml.sax.ContentHandler):

            def __init__(self, config):
                self.config = config

            def startElement(self, name, attrs):
                if name == 'pantryrc':
                    self.erasePath = True
                elif name == 'reports':
                    self.config.localReports = attrs['dir']
                elif name == 'nutrient-list':
                    self.currNutList = attrs['name']
                    self.config.nutLists[self.currNutList] = []
                elif name == 'nutrient':
                    nutName = attrs['name']
                    nutGoal = attrs.get('goal', None)
                    nutUnits = attrs['units']


                    try:
                        dictUnits = foods.NUTSBYNAME[nutName]['units']
                    except KeyError:
                        raise BadNutNameError(self.config.configFilename,
                                self.currNutList, nutName)
                    nutNo = foods.NUTSBYNAME[nutName]['no']
                    
                    if nutUnits and dictUnits != nutUnits:
                        raise BadNutUnitsError(self.config.configFilename,
                                self.currNutList, nutName, nutGoal, nutUnits,
                                dictUnits)

                    if nutGoal is not None:
                        try:
                            nutGoal = uniconv.convertUni(nutGoal)
                        except uniconv.UnconvertableError:
                            raise BadGoalError(self.config.configFilename, 
                                    self.currNutList, nutName, nutGoal)
                    self.config.nutLists[self.currNutList].append((
                        nutNo, nutGoal))
                elif name == 'sort-order':
                    self.currSortOrder = foods.TRAITSBYNAME[attrs['trait']]
                    self.config.sortOrders[self.currSortOrder] = []
                elif name == 'item':
                    value = attrs['value']
                    self.config.sortOrders[self.currSortOrder].append(
                            value)

        xml.sax.parseString(data, ConfigHandler(self))

    def __str__(self):
        import pprint
        rep = ('Configuration variables from file %s\n%s'
                % (self.configFilename, pprint.pformat(vars(self))))
        return rep

### EXCEPTIONS
class Error(Exception):
    pass

class PantryRCNotFoundError(Error):
    def __init__(self, filename):
        self.msg = [u'Pantry RC file %s not found.' % filename]

class InvalidPantryRCError(Error):
    def __init__(self, filename, e):
        self.msg = [u'Pantry RC file %s is not valid XML:' % filename]
        self.msg.extend(e.msg)

class PantryRCTooBigError(Error):
    def __init__(self, filename):
        self.msg = [u'Pantry RC file %s is too large to validate.' %
                filename]

class BadNutNameError(Error):
    def __init__(self, filename, nutList, nutName):
        self.msg = [u'Nutrient in Pantry RC file %s invalid.'
                % (filename)]
        self.msg.append('Nutrient list: %s' % nutList)
        self.msg.append('Nutrient name: %s' % nutName)

class BadNutUnitsError(Error):
    def __init__(self, filename, nutList, nutName, nutGoal, nutUnits,
            validNutUnits):
        self.msg = [u'Nutrient units in Pantry RC file %s invalid'
                % (filename)]
        self.msg.append('Nutrient list: %s' % nutList)
        self.msg.append('Nutrient name: %s' % nutName)
        self.msg.append('Nutrient goal: %s' % nutGoal)
        self.msg.append('Invalid nutrient units: %s' % nutUnits)
        self.msg.append('Only valid unit: %s' % validNutUnits)

class BadGoalError(Error):
    def __init__(self, filename, nutList, nutName, nutGoal):
        self.msg = [u'Nutrient goal in Pantry RC file %s is not a number.'
                % (filename)]
        self.msg.append('Nutrient list: %s' % nutList)
        self.msg.append('Nutrient name: %s' % nutName)
        self.msg.append('Invalid nutrient goal: %s' % nutGoal)

### CONSTANTS
DTD = """
<!ELEMENT pantryrc                  (reports?, nutrient-list*, sort-order*)>
<!ELEMENT reports                   EMPTY>
<!ELEMENT nutrient-list             (nutrient*)>
<!ELEMENT nutrient                  EMPTY>
<!ELEMENT sort-order                (item+)>
<!ELEMENT item                      EMPTY>

<!ATTLIST reports
    dir                             CDATA #REQUIRED >
<!ATTLIST nutrient-list
    name                            CDATA #REQUIRED >
<!ATTLIST nutrient
    name                            CDATA #REQUIRED
    units                           CDATA #REQUIRED
    goal                            CDATA #IMPLIED >
<!ATTLIST sort-order
    trait                           CDATA #REQUIRED >
<!ATTLIST item
    value                           CDATA #REQUIRED > """.strip()

DEFAULTS = {
'localReports':'',
'nutLists': {
    'facts' : (
        ('Calories', 2000),
        ('Total Fat', 65),
        ('Saturated Fat', 20),
        ('Cholesterol', 300),
        ('Sodium', 2400), 
        ('Total Carbohydrate', 300),
        ('Dietary Fiber', 25),
        ('Sugars', None),
        ('Protein', 50),
        ('Vitamin A', 5000),
        ('Vitamin C', 60),
        ('Calcium', 1000),
        ('Iron', 18) 
        ),

    'short' : (
        ('Calories', None),
        ('Total Fat', None),
        ('Total Carbohydrate', None),
        ('Protein', None)
        ),

    'dv' : sorted([(nut['name'], nut['dv']) for nut
        in foods.NUTSBYNO.values()
        if nut['dv'] ]),

    'all' : sorted(
            [ (nut['name'], None) for nut
                in foods.NUTSBYNO.values() ])
    },
'sortOrders':{},
}
for _nutListName, _nutList in DEFAULTS['nutLists'].items():
    DEFAULTS['nutLists'][_nutListName] = \
            [(foods.NUTSBYNAME[_nutName]['no'], goal)
                    for _nutName, goal in _nutList]
