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

""" Main Pantry command-line interface.
    
It is meant to be called using a trivial Python script that
may be installed in the user's PATH. """

from __future__ import division

def main(argv):
    """ argv is the entire argv of the calling script.

    Thus argv must include sys.argv[0] as well as the rest of the argv. """
    from optparse import OptionParser
    import errno
    import os
    import sys
    import textwrap
    
    from pantry.lib import cabinets
    from pantry.lib import foods
    from pantry.lib import io
    from pantry.lib import uniconv
    from pantry.lib import version
    import config
    import reportPrinter
    import inspect

    TRAITSBYNAME = foods.TRAITSBYNAME

    prog = os.path.basename(argv[0])
    usage = 'usage: %prog [options] FILE...'

    parser = OptionParser(prog = prog, usage = usage)

    # search options
    parser.add_option('-i', '--ignore-case', action='store_const',
            const=cabinets.IGNORECASE,
            dest='ignoreCase', 
            help="make searches case insensitive")
    parser.add_option('-x', '--exact-match', action='store_const',
            const=cabinets.EXACTMATCH,
            dest='exactMatch',
            help='traits must exactly match search term specified')
    parser.add_option("-n", "--name", action="store",
            type="string", dest="name", 
            metavar='regexp',
            help='select foods with matching name')
    parser.add_option('-g', '--group', action="store",
            type='string', dest='group', 
            metavar='regexp',
            help='select foods with matching group')
    parser.add_option("-d", "--date", action="store",
            type="string", dest='date', 
            metavar='regexp',
            help='select foods with matching date')
    parser.add_option('-m', '--meal', action='store',
            type='string', dest='meal', 
            metavar='regexp',
            help='select foods with matching meal')
    parser.add_option('-u', '--unit', action='store',
            type='string', dest='unit', 
            metavar='regexp',
            help='select foods with matching unit')
    parser.add_option('-q', '--qty', action='store',
            type='string', dest='qty', 
            metavar='number',
            help='select foods with matching qty')
    parser.add_option('-c', '--comment', action='store',
            type='string', dest='comment',
            metavar='regexp',
            help='select foods with matching comment')
    parser.add_option('-o', '--order', action='store',
            type='string', dest='order',
            metavar='regexp',
            help='select foods with matching order')
    parser.add_option('-p', '--print', action='store',
            type='string', dest='report', metavar = 'REPORT',
            help='print REPORT')
    parser.add_option('-l', '--nutrient-list', action='store',
            type='string', dest='nutList', metavar='NUTRIENT-LIST',
            help='use NUTRIENT-LIST when printing report (default facts)')
    parser.add_option('-a', '--add', action='append',
            type='string', dest='toAdd', metavar='FILE',
            help='add results to FILE')
    parser.add_option('-s', '--sort', action='store', type='string',
            dest='sort', metavar='traits',
            help='sort foods in report by these traits')
    parser.add_option('--force-valid', action='store_const',
            const = cabinets.BIGVALID, dest='force',
            help='validate all XML files, even if above 300 kilobytes')
    parser.add_option('--skip-valid', action='store_const',
            const = cabinets.SKIPVALID, dest = 'skip',
            help = 'do not validate any XML files')
    
    # change options
    parser.add_option('-N', '--c-name', action='store',
            type='string', dest='c_name', metavar='string',
            help='change name to string')
    parser.add_option('-G', '--c-group', action='store',
            type='string', dest='c_group', metavar='string',
            help='change group to string')
    parser.add_option('-Q', '--c-qty', action='store',
            type='string', dest='c_qty', metavar='number',
            help='change quantity to number')
    parser.add_option('-U', '--c-unit', action='store',
            type='string', dest='c_unit', metavar='regexp',
            help='change unit to one matching regexp')
    parser.add_option('-D', '--c-date', action='store',
            type='string', dest='c_date', metavar='string',
            help='change date to string')
    parser.add_option('-M', '--c-meal', action='store',
            type='string', dest='c_meal', metavar='string',
            help='change meal to string')
    parser.add_option('-C', '--c-comment', action='store',
            type='string', dest='c_comment', metavar='string',
            help='change comment to string')
    parser.add_option('-O', '--c-order', action='store',
            type='string', dest='c_order', metavar='string',
            help='change order to string')
    parser.add_option('-A', '--auto-order', action='store_const',
            dest='autoOrder', const=cabinets.INCREMENT,
            help='automatically increment order trait')
    parser.add_option('--by-nut', action='store',
            type='string', dest='c_by_nut', metavar='nut-name qty',
            nargs=2,
            help='set quantity of food so amount of nut-name = qty')
    parser.add_option('-K', '--by-cal', action='store',
            type='string', dest='c_by_cal', metavar='calories',
            help='set quantity of food so calories equals calories')
    parser.add_option('--edit', action='store_true',
            dest='edit', help='Edit foods in place') 
    parser.add_option('--delete', action='store_true', 
            dest='delete', help='delete matching foods')
    parser.add_option('-R', '--refuse', action='store_true',
            dest='useRefuse', 
            help='reduce qty by percent refuse')

    # dump options
    parser.add_option('--dump', action='store', type='string',
            dest='dump', metavar='what-to-dump',
            help='print internal Pantry variables (details in man page)')
    parser.add_option('-v', '--version', action='store_const',
            const='version', dest='dump',
            help='Print version and copyright information and exit')

    parser.set_defaults(ignoreCase=0, exactMatch=0,
            toAdd = [], sort='', autoOrder = False,
            force = 0, skip = 0,)

    (options, posArgs) = parser.parse_args(argv[1:])

    # convert to unicode
    options = uniconv.convertOptions(options, io.ENCODING)
    posArgs = uniconv.convertList(posArgs, io.ENCODING)

    stderr = io.puni(sys.stderr)
    errors = io.ErrorPrinter(stderr, prog, io.WARNING)
    stdout = io.puni(sys.stdout)
    searchOpts = options.ignoreCase | options.exactMatch

    try:
        configVars = config.ConfigVars()
    except config.Error, e:
        errors(u'Problem loading Pantry RC file:', e, 
                type = io.ERROR)

    if options.c_by_cal:
        options.c_by_nut = [u'Calories', options.c_by_cal]

    if options.dump:
        for dumpName, whatToPrint in (('version', version.COPYRIGHT),
            ('pantryDTD', textwrap.dedent(cabinets.PANTRYDTD)),
            ('rcDTD', textwrap.dedent(config.DTD)),
            ('config', str(configVars))):
            if options.dump == dumpName:
                stdout(whatToPrint + '\n')
                sys.exit()
        errors(u'unrecognized argument for dump option.',
                type = io.ERROR)

    if len(posArgs) < 1:
        errors('you did not specify any files to search.')
    if options.ignoreCase and options.exactMatch:
        errors('--ignore-case has no effect when using --exact-match')

    newTraits = [(trait, getattr(options, 'c_%s' % trait))
            for trait in ('name', 'group', 'date', 'meal',
                'qty', 'comment', 'order')
            if getattr(options, 'c_%s' % trait) != None]
    newTraits = dict([(TRAITSBYNAME[traitName], traitValue)
        for traitName, traitValue in newTraits])

    # make sure qty is valid
    if options.c_qty:
        badQty = False
        try:
            qtyNo = uniconv.convertUni(newTraits[TRAITSBYNAME['qty']])
        except uniconv.UnconvertableError:
            badQty = True
        else:
            if qtyNo is None:
                badQty = True
        if badQty:
            errors(u'argument to --c-qty, %s, is not a valid number.'
                    % newTraits[TRAITSBYNAME['qty']], type = io.ERROR)

    searchList = [(trait, getattr(options, trait))
                for trait in ('name', 'group', 'date', 'meal',
                    'qty', 'unit', 'comment', 'order')
                if getattr(options, trait) != None]
    searchDict = dict([(TRAITSBYNAME[traitName], traitValue)
        for traitName, traitValue in searchList])

    # make sure byNut arguments are valid
    cByNutNo = None
    if options.c_by_nut:
        matchingNutNames = cabinets.listSearch(foods.NUTSBYNAME,
                options.c_by_nut[0], searchOpts)
        if len(matchingNutNames) > 1:
            errors('more than one matching nutrient for --c-by-nut argument '
                '%s. Matches:\n%s' % 
                (options.c_by_nut[0], '\n'.join(matchingNutNames)), 
                type = io.ERROR)
        elif len(matchingNutNames) == 0:
            errors('no matching nutrients for --c-by-nut'
                    ' argument %s.' % options.c_by_nut[0],
                    type = io.ERROR)
        cByNutNo = foods.NUTSBYNAME[matchingNutNames[0]]['no']
        
        badNutAmt = False
        try:
            cByNutAmt = uniconv.convertUni(options.c_by_nut[1])
        except uniconv.UnconvertableError:
            badNutAmt = True
        if cByNutAmt is None:
            badNutAmt = True
        if badNutAmt:
            errors('amount given for --c-by-nut, %s, is invalid.'
                    % options.c_by_nut[1], type = io.ERROR)

    # do we need nutrients?
    needNuts = False
    if options.report:
        if options.nutList:
            try:
                nutList = configVars.nutLists[options.nutList]
            except KeyError:
                errors('nutrient list "%s" does not exist.' % options.nutList,
                        type=io.ERROR)
        else:
            nutList = configVars.nutLists['facts']
        reporter = reportPrinter.Reports(options.report,
                prog, sys.stdout, sys.stderr, options.sort,
                configVars.sortOrders, nutList, configVars.localReports)
        needNuts = reporter.needNuts
    if options.edit or options.toAdd or options.c_by_nut:
        needNuts = True
    results = cabinets.Temp()

    toAdd=[]
    for addFileName in options.toAdd:
        try:
            addFileCabinet = cabinets.getCabinet(addFileName, 
                    options = options.force | options.skip | cabinets.CREATE)
        except cabinets.CabinetOpenError, e:
            errors(u'could not open for --add file %s' % addFileName, e)
            continue
        else:
            argList = inspect.getargspec(addFileCabinet.addFood)[0]
            if 'options' in argList:
                toAdd.append((addFileCabinet, addFileName, True))
            else:
                if options.autoOrder:
                    errors((u'--auto-order has no effect in file %s' 
                        ' it is a Text file') % addFileName)
                toAdd.append((addFileCabinet, addFileName, False))

    results = cabinets.Temp()
    for sourceFileName in posArgs:
        try:
            sourceFileCabinet = cabinets.getCabinet(sourceFileName, 
                    options = options.skip | options.force)
        except cabinets.CabinetOpenError, e:
            errors('could not open file %s' % sourceFileName, e)
            continue

        keyList = sourceFileCabinet.search(searchDict, searchOpts)

        for key in keyList:
            # change foods; fill results instance
            food = sourceFileCabinet.getFood(key, needNuts)
            food.traits.update(newTraits)

            if options.c_unit is not None:
                matchingUnits = cabinets.listSearch(food.units,
                        options.c_unit, searchOpts)
                if len(matchingUnits) > 1:
                    errors(('food %s will not be copied into the buffer.\n' 
                            'More than one matching unit for --c-unit' 
                            ' argument %s. Matches:\n%s')
                            % (food.traits.get(TRAITSBYNAME['name'],
                                '<unnamed food>'),
                                options.c_unit, '\n'.join(matchingUnits)))
                    continue
                elif len(matchingUnits) == 0:
                    errors(('food %s will not be copied into the buffer.\n' 
                        'No matching units for unit expression %s.')
                        % (food.traits.get(TRAITSBYNAME['name'],
                            '<unnamed food>'),
                            options.c_unit))
                    continue
                food.traits[TRAITSBYNAME['unit']] = matchingUnits[0]

            if cByNutNo:
                if (cByNutNo not in food.nuts) or (food.nuts[cByNutNo] == 0):
                    errors(('food %s will not be copied into the buffer.\n' 
                            'Food does not have --c-by-nut nutrient %s') 
                            % (food.traits.get(TRAITSBYNAME['name'],
                                '<unnamed food>'),
                            foods.NUTSBYNO[cByNutNo]['name']))

                food.traits[TRAITSBYNAME['qty']] =\
                        food.getQtyByNut(cByNutNo, cByNutAmt)
                    
            if options.useRefuse:
                if uniconv.convertUni(food.traits[TRAITSBYNAME['qty']]) == 0:
                    errors(('food %s will not be copied into the buffer.\n' 
                        '--refuse is invalid for this food as'
                        'its quantity is zero.') 
                        % food.traits.get(TRAITSBYNAME['name'],
                            '<unnamed food>'))
                    continue
                if TRAITSBYNAME['pctRefuse'] not in food.traits:
                    errors(('food %s will not be copied into the buffer.\n'
                    '--refuse is invalid for this food as it does not' 
                    ' have a refuse trait.') %
                    food.traits.get(TRAITSBYNAME['name'], '<unnamed food>'))
                    continue
                food.traits[TRAITSBYNAME['qty']] = food.qtyMinusRefuse
                    
            order, mutex = results.addFood(food, None, cabinets.MUTATE)
            if mutex:
                errors(('food %s had duplicate in buffer;' 
                    ' added "(Copy %s)" to comment trait') % 
                    (food.traits.get(TRAITSBYNAME['name'],
                        '<unnamed food>'), order))

            if options.edit:
                sourceFileCabinet.addFood(food, key)
            if options.delete:
                del sourceFileCabinet[key]
            
            if options.edit or options.delete:
                try:
                    sourceFileCabinet.save()
                except cabinets.ReadOnlyError:
                    errors(('file %s could not be opened'
                        ' for writing; no foods in it are edited'
                        ' or deleted') % sourceFileName)

        del sourceFileCabinet

    if options.report:
        reporter.printReports(results)

    for addFileCabinet, addFileName, useOptions in toAdd:
        # argument for oldKey is None
        argList = [None]
        if useOptions:
            argList.append(options.autoOrder | cabinets.MUTATE)
        
        for food in results.values():
            order, mutex = addFileCabinet.addFood(food, *argList)
            if mutex:
                errors(('duplicate of food %s when adding to file %s;'
                    ' added "Copy %u" to Comment trait')
                    % (food.traits.get(TRAITSBYNAME['name'],
                        '<unnamed food>'),
                        addFileName, mutex))
        try:
            addFileCabinet.save()
        except cabinets.ReadOnlyError:
            errors((u'file %s could not be opened for writing;' 
                ' no foods added to it') % addFileName )
