#!/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

    dex = mitsfs.dexdb(client=program)

    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'
        else:
            print member
            print 'Membership:', member.membership
            if member.checkouts:
                print_checkouts(member.checkouts)
            dex.db.rollback()
        print

    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),
        ]

    menu = lambda: [
        ('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),
        ] if member is None else [
        ('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),
        ]

    rmenu(menu, title=banner)


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.outto != None)

        if not book:
            break

        member = checkin_internal(book, 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.book, advanced, bookdrop)


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

    print 'Checking in: '

    # mark it as checked in:
    #  get checkout_id
    try:
        for (mem, checkout) in book.checkin(checkin_date):
            print checkout
            if checkout.lost and not mem.pseudo:
                ltxns = checkout.lost_txns
                if ltxns:
                    mem.void_transaction(ltxns[-1])
                    print 'Lost book fine refunded to fine credit'
            days = checkout.overdue_date(checkin_date)
            if days and not mem.pseudo:
                fine = min(days, 40) * .25
                print "Book is overdue", days, "days"
                if advanced:
                    fine = -mitsfs.readmoney(
                        fine, "Fine to charge to patron's account: ")
                else:
                    fine = -fine
                print 'FINE: %s added to balance' % (mitsfs.money_str(fine),)

                desc = 'Book %s overdue %d days.' % (book, days)
                mem.fine_transaction(fine, desc, checkout.checkout_id)
            print 'Book checked out to', mem, 'has been checked in'

    except mitsfs.CirculationException, exc:
        print exc
    print

    return mem

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 'FINE: %s added to balance' % (mitsfs.money_str(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):
    #Various Checks go here
    if not member.pseudo:
        if not check_balance(member):
            print "WARNING: Member", member, "has a negative balance."
            if not advanced:
                print "Correct balance or use Fancy Check Out."
                return

        if member.membership is None or member.membership.isexpired:
            print "WARNING: Member", member, "has an expired,"
            print "or nonexistent membership."
            if not advanced:
                print "Get a new membership or use Fancy Check Out."
                return

        due_books = [out for out in member.checkouts if out.due]

        if due_books:
            print "WARNING: Member", member, "has overdue books."
            if not advanced:
                for out in due_books:
                    print out

                print "Return books or use Fancy Check Out."
                return

    while True:
        count = len(member.checkouts)
        if not member.pseudo and count >= 8:
            print "Member", member, "has", count, "books out."
            print "Only 8 books are allowed out at a time."
            if not advanced:
                print "Check in books or use Fancy Check Out to check out more."
                return

        #Only Circulating books on non fancy checkoout
        if advanced:
            title_predicate = lambda title: any(book for book in title.books
                                                if not book.outto)
            book_predicate = lambda book: not book.outto
        else:
            title_predicate = lambda title: any(book for book in title.books
                                                if (not book.outto
                                                    and book.circulating))
            book_predicate = lambda book: not book.outto 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

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

        checkout_date = None
        if advanced:
            print "Specify check out date:"
            checkout_date = mitsfs.readdate(datetime.date.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 val(line):
        ok = line.strip().upper() in membook.memberships
        if not ok:
            print "Not a valid membership type"
        return ok

    membership_types = mitsfs.tabulate([
            [mitsfs.COLOR.select(k + '.'), v]
            for (k, v) in sorted(membook.memberships.items())])

    print "Select membership type:"
    print membership_types

    member_type = mitsfs.readvalidate(
        'Select Membership Type: ', validate = val).upper()

    relativeto = (
        member.membership.expires
        if member.membership and not member.membership.isexpired
        else datetime.date.today())

    if member_type == 'Y':
        print "Enter number of years:"
        for i, x in enumerate(mitsfs.rates['Y']):
            if i == 0:
                continue
            print "  %s year membership: $%s" % (i, x)
        num_years = mitsfs.readnumber('Years: ', 1, 5)
        if num_years is None:
            return
        money = mitsfs.readmoney(mitsfs.rates[member_type][num_years])
        exp = relativeto + dateutil.relativedelta.relativedelta(years=+num_years)
    else:
        money = mitsfs.readmoney(mitsfs.rates[member_type])
        if member_type == 'T':
            exp = relativeto + dateutil.relativedelta.relativedelta(months=+3)
        else:
            exp = None
    if exp is not None:
        print 'Projected expiration', exp
        exp = mitsfs.readdate(exp)

    member.membership_add(member_type, exp, money)
    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()
        c.execute(
            'insert into member_name(member_id,member_name)'
            ' values (%s,%s) returning member_name_id',
            (member.member_id, new))
        if mitsfs.readyes('Set name to default? [' + mitsfs.COLOR.yN + '] '):
            member_name_id = c.fetchone()[0]
            member.member_name_default = member_name_id
        dex.db.commit()

    def addemail(line):
        print member, 'Existing emails:'
        for x in member.emails:
            print member.pretty_email(x)
        new = mitsfs.reademail("Email to add: ")
        c.execute(
            'insert into member_email(member_id,member_email)'
            ' values (%s,%s) returning member_email_id',
            (member.member_id, new))
        if mitsfs.readyes('Set email to default? [' + mitsfs.COLOR.yN + '] '):
            member_email_id = c.fetchone()[0]
            member.member_email_default = member_email_id
        dex.db.commit()

    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()

        c.execute(
            'insert into member_address'
            ' (member_id,member_address, address_type)'
            ' values (%s,%s, %s) returning member_address_id',
            (member.member_id, new, addr_type))
        if mitsfs.readyes('Set address to default? [' + mitsfs.COLOR.yN + '] '):
            member_address_id = c.fetchone()[0]
            member.member_address_default = member_address_id
        dex.db.commit()

    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:
            c.execute(
                'delete from member_' + title +
                ' where member_' + title + '_id=%s',
                (info[delete-1].id,))
            dex.db.commit()

    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

    dex.cursor.execute(
        'insert into member default values returning member_id', ())
    member_id = dex.cursor.fetchone()[0]

    dex.cursor.execute(
        'insert into member_name(member_id, member_name)'
        ' values (%s,%s) returning member_name_id',
        (member_id, full_name))

    if nickname != '':
        dex.cursor.execute(
            'insert into member_name(member_id, member_name)'
            ' values (%s,%s) returning member_name_id',
            (member_id, nickname))

    member_name_id = dex.cursor.fetchone()[0]

    dex.cursor.execute(
        'insert into member_email(member_id, member_email)'
        ' values (%s,%s) returning member_email_id',
        (member_id, email))

    member_email_id = dex.cursor.fetchone()[0]

    addr = '\n'.join(addr).strip()
    dex.cursor.execute(
        'insert into member_address(member_id, member_address, address_type)'
        ' values (%s,%s,%s) returning member_address_id',
        (member_id, addr, 'P'))

    member_address_id = dex.cursor.fetchone()[0]

    dex.cursor.execute(
        'update member'
        ' set member_name_default = %s,'
        ' member_email_default = %s,'
        ' member_address_default = %s'
        ' where member_id = %s',
        (member_name_id, member_email_id, member_address_id, member_id))

    dex.db.commit()
    global member
    member = mitsfs.member(dex, member_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
        outto = book.outto
        if outto:
            print "    " + str(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
    print
    print offset + 'Author ' + ' ' * (ll - 12) + 'Title'
    print offset + ' ' * (ll - 43) + 'Code   Check Out         Check In/Due'
    print offset + '=' * ll
    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.lost:
            duedate = c.lost.date()
            duestr = mitsfs.Color.warning('LOST')
        else:
            duestr = 'Due:'
            duedate = color_due_date(c.due_date)
        if c.checkin_stamp:
            duestr = c.checkin_user
            duedate = c.checkin_stamp.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)
