#!/usr/bin/python

__version__='$Rev: 552 $'
__release__='1.1'

program = 'greendex'

import sys
import os
import optparse
import logging
import traceback
import datetime

import dateutil.relativedelta

import mitsfs

if 'dex' in locals():
    del dex
dex = None

parser = optparse.OptionParser(
    usage = 'usage: %prog [options]',
    version ='%prog '+'$Id$')

member = None

def main(args):
    global dex,  membook, member

    try:
        dex = mitsfs.dexdb(client=program)
    except Exception as e:
        # The traceback is unlikely to be nearly so useful as the error
        # message, and will cause people to miss the meat of the error message
        print str(e)
        exit(1)

    membook = dex.membook()

    options, args = parser.parse_args(args)

    if len(args) != 1:
        mitsfs.banner(program, __release__, __version__)
        parser.print_usage()
        sys.exit(1)

    mitsfs.banner(program, __release__, __version__)

    if dex.dsn != mitsfs.database_dsn:
        print '(' + dex.dsn + ')'

    def banner():
        if member is None:
            print 'Main Menu'
            print
        else:
            lm = mitsfs.len_color_str(member)
            ls = mitsfs.len_color_str(member.membership)
            ll = min(mitsfs.termwidth(), 80) - 1
            print '%s %*s%s' % (
                member,
                ll - lm  - ls - 13, '',
                 'Membership: ' + str(member.membership))
            if member.checkouts:
                print_checkouts(member.checkouts)
            dex.db.rollback()

    advanced = lambda: [
        ('B', 'Book Drop Check In',
         lambda line: checkin(line, bookdrop = True)),
        ('I', 'Fancy Check In',
         lambda line: checkin(line, advanced = True)),
        ('Q', 'Main Menu', None),
        ] if member is None else [
        ('B', 'Book Drop Check In',
         lambda line: checkin_member(line, bookdrop = True)),
        ('I', 'Fancy Check In',
         lambda line: checkin_member(line, advanced = True)),
        ('O', 'Fancy Check Out',
         lambda line: checkout(line, advanced = True)),
        ('Q', 'Main Menu', None),
        ]

    member_menu = [
        ('O', 'Check Out Books by Book', checkout),
        ('I', 'Check In Books by Patron', checkin_member),
        ('L', 'Declare Books Lost', lost),
        ('V', 'View Patron', viewmem),
        ('E', 'Edit Patron and Membership', editmem),
        ('P', 'Pay Outstanding Fines',
         lambda x: check_balance(member, print_notices = True)),
        ('F', 'Financial Transaction', financial),
        ('A', 'Advanced (Book Drop/Fancy Check In/Check Out)',
         lambda x: rmenu(advanced, x, title = "Advanced")),
        ('S', 'Select Patron', select),
        ('Q', 'Unselect Patron', unselect),
        ]

    nomember_menu = [
        ('S', 'Select Patron', select),
        ('I', 'Check In Books', checkin),
        ('N', 'New Patron', newmem),
        ('D', 'Display Book', display),
        ('A', 'Book Drop/Fancy Check In',
         lambda line: rmenu(
             advanced, line, title = "Fancy/Book Drop Check In:")),
        ('Q', 'Quit', None),
        ]
    nomember_keys = set(x[0] for x in nomember_menu)
    nomember_menu += [
        (x[0], '', please) for x in member_menu if x[0] not in nomember_keys]

    menu = lambda:  nomember_menu if member is None else member_menu

    rmenu(menu, title=banner)


def please(line):
    print "Please select a member first"
    print


def select(line):
    global member

    line = line.strip()

    if line:
        possibles = membook.search(line)
        if len(possibles) == 1:
            member = possibles[0]
            return
    member = mitsfs.specify_member(membook, line)


def unselect(line):
    global member
    member = None


def checkin(line, advanced = False, bookdrop = False):
    global member

    while True:
        book = mitsfs.specify_book(
            dex,
            authorcomplete = dex.indices.authors.complete_checkedout,
            titlecomplete = dex.indices.titles.complete_checkedout,
            title_predicate = lambda title: title.checkedout,
            book_predicate = lambda book: book.out)

        if not book:
            break

        checkouts = book.checkouts
        if len(checkouts) > 1:
            print 'Warning: %s is checked out more than once' % (book,)
        for checkout in checkouts:
            member = checkin_internal(checkout, advanced, bookdrop)


def checkin_member(line, advanced = False, bookdrop = False):
    while True:
        if not member.checkouts:
            print 'No books are checked out.'
            break

        checkout = select_checkedout('Select book to check in: ')

        print

        if checkout == None:
            return

        checkin_internal(checkout, advanced, bookdrop)


def checkin_internal(checkout, advanced, bookdrop):
    checkin_date = None
    if advanced or bookdrop:
        print "Specify check in date:"
        checkin_date = mitsfs.readdate(datetime.datetime.today(), False)

    print 'Checking in: '

    try:
            print checkout
            print checkout.checkin(checkin_date)
    except mitsfs.CirculationException, exc:
        print exc
        print 'Book NOT checked in.'
    print
    return checkout.member


def lost(line): #What?
    if not member.checkouts:
        print 'No books are checked out.'
        return

    while True:
        if not [checkout for checkout in member.checkouts if not checkout.lost]:
            break

        checkout = select_checkedout('Select book to declare as lost: ')
        print

        if checkout is None:
            return

        if checkout.lost:
            print 'That book is already lost.  To unlose it, check it in.'
            continue

        print checkout.lose()


def select_checkedout(prompt):
    print_checkouts(checkouts=member.checkouts, enum=True)
    print mitsfs.Color.select('Q.'), 'Back to Main Menu'
    print

    num = mitsfs.readnumber(
        prompt,
        1,
        len(member.checkouts) + 1,
        escape = 'Q')

    if num == None:
        return None

    return member.checkouts[num -1]


def checkout(line, advanced=False):
    # move this logic to the library
    if not member.pseudo:
        check_balance(member)

    while True:
        ok, msgs, correct = member.checkout_good(advanced)

        if not ok:
            if advanced:
                print '\n'.join('WARNING: ' + msg for msg in msgs)
            else:
                print '\n'.join(msgs)
                print
                print correct + ' or use Fancy Check Out.'
                return

        #Only Circulating books on non fancy checkoout
        if advanced or member.pseudo:
            title_predicate = lambda title: any(book for book in title.books
                                                if not book.out)
            book_predicate = lambda book: not book.out
        else:
            title_predicate = lambda title: any(book for book in title.books
                                                if (not book.out
                                                    and book.circulating))
            book_predicate = lambda book: not book.out and book.circulating

        print "Check out books for member", str(member)
        print
        book = mitsfs.specify_book(
            dex, # predicate for not in select book_id in checkout
                 #  where checkin_stamp is not null
                 # is too much cpu for not enough benefit
            title_predicate = title_predicate,
            book_predicate =  book_predicate,
            )

        if not book:
            break

        if book.out:
            print book
            print 'is already checked out to', book.outto
            return

        checkout_date = None
        if advanced:
            print "Specify check out date:"
            checkout_date = mitsfs.readdate(datetime.datetime.today(), False)

        checkout = book.checkout(member, checkout_date)
        print 'Checking out:'
        print checkout


def barcodebook(book):
    if len(book.barcodes) == 0:
        print
        print "Book has no barcode.  Please attach and scan new barcode."
        while True:
            barcode = mitsfs.readbarcode()
            if barcode is None:
                break
            if book.addbarcode(barcode):
                if len(book.barcodes) > 1:
                    print """
WARNING: book has acquired two barcodes when it had zero
moments ago; please look to your left or right and see if
someone is checking out a similar book and role-play
accordingly; otherwise please let libcomm know that they
need to go meditate on the database logs."""
                break
            print "Error adding barcode; perhaps it is already in use."


def viewmem(line):
    def fin(line):
        print 'Transactions of ', member
        print mitsfs.tabulate(
            [('Amount', 'Keyholder', 'Date', 'Type', 'Description')]
            + [(mitsfs.money_str(amount), by, when.date(),
                membook.txn_types[txn_type], desc)
               for (amount, desc, txn_type, by, when) in member.transactions])

    def history(line):
        print "History of: ", str(member)
        print_checkouts(checkouts = member.checkout_history)

    def mem(line):
        print str(member), " Membership History:"
        print mitsfs.tabulate(
            [("Membership History", "Keyholder", "Bought")]+
            [(str(m), str(m.created_by), str(m.created.date()))
             for m in member.memberships])

    print
    print member.info()

    rmenu([
        ('C', 'Check Out History', history),
        ('F', 'Financial History', fin),
        ('M', 'Membership History', mem),
        ('Q', 'Main Menu', None),
        ], title = "View User/Patron:")


def membership(line):
    def validate(line):
        line = line.strip().lower()
        if len(line) == 1 \
          and 0 <= (ord(line) - ord('a')) < len(membook.membership_types):
            return True
        print 'Input must be a letter between a and', \
          chr(ord('a') + len(membook.membership_types) - 1)
        return False

    print "Select membership type:"
    print mitsfs.tabulate([
        [mitsfs.COLOR.select(chr(ord('a') + n) + '.'), d, '$%.2f' % c]
        for (n, (t, d, c)) in enumerate(membook.membership_types)])

    member_type_char = mitsfs.readvalidate(
        'Select Membership Type: ', validate = validate).lower()
    member_type = membook.membership_types[ord(member_type_char) - ord('a')][0]

    description, cost, expiration = member.membership_describe(member_type)
    msg = '%s membership would cost $%.2f' % (description, cost)
    if expiration:
        mitsfs.lfill((msg + ' and expire at').split() + [str(expiration) + '.'])
    else:
        mitsfs.pfill(msg + '.')

    if mitsfs.readyes('Continue? [' + mitsfs.COLOR.yN + '] '):
        member.membership_add(member_type)
        check_balance(member, 'Membership Payment')


def editmem(line):
    c = dex.getcursor()
    if member.pseudo:
        print "WARNING editing pseudo account: %s is disallowed." % (member,)
        print "Email libcomm@mit.edu if you need to modify information"
        print "in a pseudo user account."
        return

    def addname(line):
        print member, 'Existing names:'
        for x in member.names:
             print member.pretty_name(x)
        new = mitsfs.readvalidate("Name to add: ").strip()

        o = mitsfs.member_name(dex)
        o.member_id = member.id
        o.member_name = new
        o.create()

        if mitsfs.readyes('Set name to default? [' + mitsfs.COLOR.yN + '] '):
            member.member_name_default = o.id

    def addemail(line):
        print member, 'Existing emails:'
        for x in member.emails:
            print member.pretty_email(x)
        new = mitsfs.reademail("Email to add: ")

        o = mitsfs.member_email(dex)
        o.member_id = member.id
        o.member_email = new
        o.create()

        if mitsfs.readyes('Set email to default? [' + mitsfs.COLOR.yN + '] '):
            member.member_email_default = o.id

    def addaddress(line):
        print member, "Existing addressess:"

        for x in member.addresses:
             print member.pretty_address(x)

        (addr_type, new) = mitsfs.readaddress(membook.address_types)

        print 'Adding', membook.address_types[addr_type]
        new = '\n'.join(new).strip()

        o = mitsfs.member_address(dex)
        o.member_id = member.id
        o.member_address = new
        o.address_type = addr_type
        o.create()

        if mitsfs.readyes('Set address to default? [' + mitsfs.COLOR.yN + '] '):
            member.member_address_default = o.id

    def remove(line, title, info):
        if len(info) == 0:
            print "No non-default", title, "to remove"
            return
        print "Remove a non-default", title + ":"
        table = []
        for n, x in enumerate(info):
            lines = str(x).split("\n")
            table += [(mitsfs.COLOR.select('%d.' % (n + 1,)), lines[0])]
            table += [('', line) for line in lines[1:]]
        table += [(mitsfs.COLOR.select('Q.'), 'Back to Remove Menu')]
        print mitsfs.tabulate(table)
        print

        delete = mitsfs.readnumber(
            'Select %s to delete: ' % (title,),
            0 , len(info) + 1, escape = 'Q')

        if delete == None:
            print 'Nothing removed.'
            return
        else:
            info[delete - 1].delete()

    def default(line, title, info, field):
        if len(info) == 0:
            print "No", title, "to set as default"
            return
        print "Set Default", title + ":"
        table = []
        for n, x in enumerate(info):
            lines = str(x).split("\n")
            table += [(mitsfs.COLOR.select('%d.' % (n + 1,)), lines[0])]
            table += [('', line) for line in lines[1:]]
        table += [(mitsfs.COLOR.select('Q.'), 'Back to Set Default Menu')]
        print mitsfs.tabulate(table)
        print

        select = mitsfs.readnumber(
            'Select %s to set as default: ' % (title,),
            0, len(info) + 1, escape = 'Q')

        if select == None:
            print 'Nothing selected.'
            return
        else:
            field(info[select-1].id)

    def set_default_name(name):
        member.member_name_default = name

    def set_default_email(email):
        member.member_email_default = email

    def set_default_address(address):
        member.member_address_default = address

    def add_info(line):
         rmenu([
             ('N', 'Add Name', addname),
             ('E', 'Add Email', addemail),
             ('A', 'Add Address', addaddress),
             ('Q', 'Back to Edit Membership', None)
             ], title = 'Add Patron Information')

    def remove_info(line):
         rmenu([
             ('N', 'Remove Name',
              lambda x: remove(x, 'name', member.other_names)),
             ('E', 'Remove Email',
              lambda x: remove(x, 'email', member.other_emails)),
             ('A', 'Remove Address',
              lambda x: remove(x, 'address', member.other_addresses)),
             ('Q', 'Back to Edit Membership', None),
             ], title = 'Remove Patron Information')

    def set_default_info(line):
         rmenu([
             ('N', 'Set Default Name',
              lambda x: default(x, 'name', member.names, set_default_name)),
             ('E', 'Set Default Email',
              lambda x: default(x, 'email', member.emails, set_default_email)),
             ('A', 'Set Default Address',
              lambda x: default(
                  x, 'address', member.addresses, set_default_address)),
             ('Q', 'Back to Edit Membership', None)
             ], title = 'Set Default Patron Information')

    print
    print member.info()
    rmenu([
        ('M', 'New/Renew Membership', membership),
        ('A', 'Add Info', add_info),
        ('R', 'Remove Info', remove_info),
        ('D', 'Set Default Info', set_default_info),
        ('Q', 'Main Menu', None),
        ], title = 'Membership')


def newmem(line):
    print "Please transfer the patron's information from the sheet."

    full_name = mitsfs.readvalidate('Legal name (required): ').strip()

    names = membook.search(full_name)
    if len(names) > 0:
        print "The following people are already in greendex:"
        for n in names:
            print "    " + str(n)
        print 'Are your sure you want to continue, instead of adding a'
        print 'membership in the edit menu?'
        if not mitsfs.readyes('Continue? [' + mitsfs.COLOR.yN + '] '):
            return
    nickname = mitsfs.read("Nickname: ").strip()
    email = mitsfs.reademail("Email (required): ")

    print
    print "Please enter a real address which will be valid for the longest term:"
    print

    (addr_type, addr) = mitsfs.readaddress(membook.address_types)

    if not mitsfs.readyes('Add this patron? [' + mitsfs.COLOR.yN + '] '):
        return

    newmember = mitsfs.member(dex)
    newmember.create(commit = False)

    member_name = mitsfs.member_name(
        dex, member_id=newmember.id, member_name=full_name)
    member_name.create(commit=False)
    newmember.member_name_default = member_name.id

    if nickname:
        nick = mitsfs.member_name(
            dex, member_id=newmember.id, member_name=nickname)
        nick.create(commit = False)

    member_email = mitsfs.member_email(
        dex, member_id=newmember.id, member_email=email)
    member_email.create(commit=False)
    newmember.member_email_default = member_email.id

    member_addr = mitsfs.member_address(
        dex, member_id=newmember.id,
        member_address='\n'.join(addr).strip(), address_type='P')
    member_addr.create(commit=False)
    member_address_default = member_addr.id

    newmember.commit()

    global member
    member = mitsfs.member(dex, newmember.id)

    print
    print 'Member added.'
    print

    if mitsfs.readyes(
        'Add a membership to new patron? [' + mitsfs.COLOR.yN + '] '):
        membership(None)


def financial(line):
    menu = sorted(
        (k, v, lambda x, k = k: do_transaction(k))
        for (k, v) in membook.basic_transactions.items())
    menu += [
        ('A', 'Advanced Transactions', financial_other),
        ('Q', 'Back to Main Menu', None),
        ]

    rmenu(menu, title = 'Finanical Transactions')


def financial_other(line):
    menu = sorted(
        (k, v, lambda x, k = k: do_transaction(k))
        for (k, v) in membook.fancy_transactions.items())
    menu += [
        ('Q', 'Back to Financial Transactions', None),
        ]

    rmenu(menu, title = "Advanced Financial Transactions Menu:")


def do_transaction(txntype):
    print
    print 'Transaction for', member
    print

    c = dex.getcursor()

    if txntype == 'V':
        txns = member.non_void_transactions

        if len(txns) == 0:
            print "No transactions to void."
            return

        quit_item = (mitsfs.COLOR.select('Q.'), 'Back to Main Menu')

        print 'Non-void Transactions of ', member
        print mitsfs.tabulate(
            [('#', 'Amount', 'Keyholder', 'Date', 'Type', 'Description')]
            + [(mitsfs.COLOR.select(str(i + 1) + '.'), mitsfs.money_str(tx[1]),
                tx[4], tx[5].date(), membook.txn_types[tx[3]], tx[2])
               for (i, tx) in enumerate(txns)] + [quit_item])

        num = mitsfs.readnumber(
            'Select transaction to void: ', 1 , len(txns) + 1, escape = 'Q')

        if num != None:
            print
            voided = member.void_transaction(txns[num-1][0])
            print "Voided transactions:"
            print mitsfs.tabulate(
                [('Member', 'Amount', 'Keyholder', 'Date', 'Type',
                  'Description')]
                + [(mitsfs.member(dex, mem_id).name, mitsfs.money_str(amount),
                    by, when.date(), membook.txn_types[txn_type], desc)
                   for (amount, desc, txn_type, by, when, mem_id) in voided])
        return

    if txntype in ['D', 'P']:
        if txntype == 'D':
            print 'Enter amount of donation, this will increase',
            print "the patron's balance."
        else:
            print 'Enter the amount being paid, this will increase',
            print "the patron's balance."
        amount = mitsfs.readmoney(prompt = 'Amount: ').copy_abs()
        print amount
    elif txntype in ['K', 'F', 'R', 'M']:
        if txntype in ['K', 'F']:
            print 'Enter the fine amount, this will decrease',
            print "the patron's balance."
        elif txntype == 'M':
            print """Warning, this does not update the patron's membership.
All this does is create a transaction with the type 'membership'.
If you want to update a membership, go to 'Edit Member' and add
a new membership; that will automatically create a new transaction.

Enter an amount; this will decrease the patron's balance."""
        else:
            print """Enter the amount the patron is being reimbursed,
this will decrease the patron's balance."""
        amount = -mitsfs.readmoney(prompt = 'Amount: ').copy_abs()
    else:
        print 'Enter amount (negative for fines, positive for credit).'
        amount = mitsfs.readmoney(prompt = 'Amount: ')

    desc = mitsfs.read('Enter description: ', history='description')

    print 'Adding %s to account of %s.' % (mitsfs.money_str(amount), member)

    if txntype  in ['P', 'R']:
        print 'Adding %s to cash drawer' % (mitsfs.money_str(amount),)

    if not mitsfs.readyes('Commit the transaction? [' + mitsfs.COLOR.yN + '] '):
        return

    if txntype not in ['P', 'R']:
        member.transaction(amount, txntype, desc)
    else:
        cash_desc = "Cash transaction for %s: %s" % (member.normal_str, desc)
        member.cash_transaction(amount, txntype, cash_desc)


def check_balance(member, desc = "Payment", print_notices = False):
    if member.pseudo:
        if print_notices:
            print "Pseudo-member, can't change balances."
        return True
    amount = -member.balance

    if amount > 0:
        print 'Member', member, 'has a negative balance'
        if mitsfs.readyes('Pay balance? [' + mitsfs.COLOR.yN + '] '):
            amount = mitsfs.readmoney(
                amount,
                prompt2 = 'Is member paying %s? [' + mitsfs.COLOR.yN + '] ',
                prompt = 'Amount they are paying: ')

            desc = desc + ' by ' + member.normal_str
            member.cash_transaction(amount, 'P', desc)
    elif print_notices:
        print "Member doesn't have a negative balance"

    return member.balance >= 0


def display(line):
    title = mitsfs.specify(dex)
    if not title:
        return

    print title
    print
    print 'HOLDINGS - If book is checked out, the member it is checked out to'
    print 'will be on the next line.'
    for book in title.books:
        print book
        if book.out:
            print '    ', book.outto
    print


def color_due_date(stamp):
    return (
        mitsfs.Color.good
        if datetime.datetime.now() < stamp
        else mitsfs.Color.warning)(stamp.date())


def print_checkouts(checkouts, enum = False):
    ll = min(mitsfs.termwidth(), 80) - 1
    offset = ''
    if enum:
        # We're assuming here that this is for the checkin-by-member function.
        # Normal users shouldn't have more than eight books out.  Abnormal
        # users can deal with a little bit of ugly.
        offset = '   '
        ll -= 3
    mitsfs.bold()
    print offset + 'Author ' + ' ' * (ll - 12) + 'Title'
    mitsfs.smul()
    print (
        offset + ' ' * (ll - 43) + 'Code' + (3 * ' ') + 'Check Out' + (9 * ' ')
        + 'Check In/Due' + (6 * ' '))
    mitsfs.sgr0()
    for n, c in enumerate(list(checkouts)):
        title = c.book.title.titletxt
        if c.book.visible:
            title = c.book.title.seriestxt + ': ' + title

        author = c.book.title.authortxt
        width = len(title) + len(author)

        if enum:
            print '%d.' % (n + 1),

        if width <= ll - 1:
            print author + ' ' * (ll - width) + title
        else:
            print author
            print offset + ' ' + ' ' * (ll - len(title) - 1) + title

        if c.checkin_stamp:
            duestr = c.checkin_user
            duedate = c.checkin_stamp.date()
        elif c.lost:
            duedate = c.lost.date()
            duestr = mitsfs.Color.warning('LOST')
        elif c.member.pseudo:
            duestr = ''
            duedate = ''
        else:
            duestr = 'Due:'
            duedate = color_due_date(c.due_date)

        print offset + ' %*s %8s %s %s %s' % (
            ll - 41,
            str(c.book.shelfcode) + ((' ' + c.book.barcodes[-1])
                                     if c.book.barcodes else ''),
            c.checkout_user,
            c.checkout_stamp.date(),
            max(8-mitsfs.len_color_str(duestr), 0)*' ' + duestr,
            duedate,
            )
    dex.db.rollback()


def rmenu(*args, **kw):
    return mitsfs.menu(*args, cleanup=dex.db.rollback, **kw)


if __name__ == '__main__':
    main(sys.argv)
