#!/usr/bin/python
'''Print a pinkdex.'''


import optparse
import os
import re
import sys
import time

import mitsfs


__version__ = '0'


parser = optparse.OptionParser(
    usage='usage: %prog [--datadex file]',
    version='%prog ' + __version__)
parser.add_option(
    '-p', '--predicate', dest='predicate',
    help='SQL predicate for dex', default=None)
parser.add_option(
    '-S', '--shelfcode', dest='shelfcodes', action='append',
    help='Make a shelfdex', default=[])
parser.add_option(
    '-s', '--supplement', dest='suppl',
    help='supplementary dex', default=None)
parser.add_option(
    '-o', '--outfile', dest='outfile',
    help='Output file', default=None)
parser.add_option(
    '-a', '--add', dest='add',
    help='Datadex format file to merge')
parser.add_option(
    '-H', '--hassle', action='store_true', dest='hassle',
    help="Stick other holdings in shelfdex for HassleComm's convenience")
parser.add_option(
    '-i', '--inventory', dest='inventory',
    help='Specify inventory tag')
parser.add_option(
    '-I', '--inventory-description', dest='inventory_desc',
    help='Specify inventory display name if initializing an inventory tag')
parser.add_option(
    '-d', '--directory', dest='directory',
    help='Specify target directory')
parser.add_option(
    '-D', '--downcase', action="store_true", dest='downcase', default=False,
    help='Attempt to downcase the dex')
parser.add_option('-K', '--boxdex', type='int', dest='boxdex')
parser.add_option(
    '--dsn', dest='dsn',
    default=os.environ.get('MITSFS_DSN') or mitsfs.DATABASE_DSN)
parser.add_option('--stop', dest='stop', action='store_true')
options, args = parser.parse_args()

noboxed = True

d = mitsfs.DexDB(dsn=options.dsn)

if options.inventory:
    try:
        inventory = mitsfs.Inventory(
            d, options.inventory, options.inventory_desc)
    except mitsfs.InventoryUnknown, e:
        print 'Unknown inventory %s;' % e.code
        print 'Please supply a description with -I so we can initialize it.'
        sys.exit(1)
else:
    inventory = None

if options.stop:
    exit(0)

if options.directory:
    if not os.path.isdir(options.directory):
        os.mkdir(options.directory)
    os.chdir(options.directory)

if os.getcwd() == mitsfs.DEXBASE:
    print 'Preemptively changing directory to /tmp;',
    print 'look for your pinkdexen there.'
    os.chdir('/tmp')


def titlecase(s):
    if options.downcase:
        return re.sub(
            '\'([SDT]|Ll|Re)([^A-Z]|$)',
            lambda m: m.group(0).lower(),
            s.title())
    else:
        return s


def nicetitle(line):
    series = [titlecase(i.replace(',', r'\,')) for i in line.series if i]
    titles = [  # strip the sortbys
        titlecase('=' in i and i[:i.find('=')] or i) for i in line.titles]
    if series:
        if len(series) == len(titles):
            titles = ['%s [%s]' % i for i in zip(titles, series)]
        elif len(titles) == 1:
            titles = ['%s [%s]' % (titles[0], '|'.join(series))]
        elif len(series) == 1:
            titles = ['%s [%s]' % (i, series[0]) for i in titles]
        else:  # this is apparently Officially Weird
            print 'Wacky title/series match: ', str(line)
            ntitles = ['%s [%s]' % i for i in zip(titles, series)]
            if len(line.series) < len(titles):
                ntitles += titles[len(series):]
            titles = ntitles
    return '|'.join(titles)


def book(line):
    if noboxed:
        unboxed = [
            (code, count)
            for (code, count) in line.codes.items()
            if mitsfs.codes[mitsfs.splitcode(code)[1]].get('box')
            not in ('all', 'kbx')]
        nboxed = [
            (code, count)
            for (code, count) in line.codes.items()
            if mitsfs.codes[mitsfs.splitcode(code)[1]].get('box') == 'all']
        if unboxed:
            codes = ','.join((
                count == 1 and code or r'%s\:%d' % (code, count)
                for (code, count) in unboxed))
        elif nboxed:
            codes = r'\[' + ','.join((
                count == 1 and code or r'%s\:%d' % (code, count)
                for (code, count) in nboxed)) + r'\]'
        else:
            codes = '*'
    else:
        codes = str(line.codes).replace(':', r'\:')
    args = [
        mitsfs.texquote(i)
        for i in [titlecase(line.authortxt), nicetitle(line), codes]]
    return r'\Book' + ''.join(['{%s}' % i for i in args])


nwords = {
    '0': 'zero', '1': 'one', '2': 'two', '3': 'three',
    '4': 'four', '5': 'five', '6': 'six', '7': 'seven',
    '8': 'eight', '9': 'nine'}


def dobar(n):
    return r'\barA' + ''.join([r'\bar' + nwords[i] for i in str(n)]) + r'\barA'


def progress_meter(it, divisor=1000):
    count = 0
    for i in it:
        if count % divisor == 0:
            print count
        count += 1
        yield i


def mungedex(query=None, args=[], add=None):
    # select distinct title_id from title natural join book where not withdrawn
    print 'constructing subset:'
    dl = list(progress_meter(d.iter(query, args)))
    dl = [mitsfs.DexLine(i) for i in progress_meter(dl)]
    if add:
        dex = mitsfs.Dex(dl)
        print 'adding %s...' % add
        sys.stdout.flush()
        dex.merge(mitsfs.Dex(add))
        dl = list(dex)
        print 'done.'

    return dl


def mungeshelf(shelfcodes):
    # select distinct title_id, count(book_id) from title natural join book
    # where not withdrawn ... group by title_id
    query = (
        'select title_id, book_series_visible, doublecrap, count(book.book_id)'
        ' from book natural join shelfcode'
        '  left join checkout on book.book_id = checkout.book_id'
        ' and checkin_stamp is null'
        ' where not withdrawn'
        '  and (' + ' or '.join(['shelfcode=%s'] * len(shelfcodes)) + ')'
        '  and checkout_stamp is null'
        ' group by title_id, book_series_visible, doublecrap')
    query = (
        'select title_id, book_series_visible, doublecrap, count(book.book_id)'
        ' from book natural join shelfcode'
        ' where not withdrawn'
        '  and (' + ' or '.join(['shelfcode=%s'] * len(shelfcodes)) + ')'
        ' group by title_id, book_series_visible, doublecrap')
    args = shelfcodes
    print 'mungeshelf', query

    def constructor(id, bsv, dc, c):
        es = ('@' if bsv else '') + shelfcodes[0] + (dc if dc else '')
        t = mitsfs.Title(d, id)
        dl = mitsfs.DexLine(
            authors=t.authors, titles=t.titles, series=t.series, codes={es: c})
        dl.othercount = sum(t.codes[i] for i in shelfcodes[1:])
        return dl.shelfkey(es), dl

    print 'constructing subset:'
    dl = list(progress_meter(
        constructor(id, bsv, dc, c)
        for id, bsv, dc, c in d.getcursor().execute(query, args)))
    print 'sorting:',
    dl.sort(key=lambda x: x[0])
    print 'done.'

    return [i[1] for i in dl]


def writedex(
        outfile, books, shelfcode=None, suppl=None, hassle=None, reverse=False,
        dexname='Pinkdex', letterfield='placeauthor', blob=None, kbx=None):
    if suppl:
        if shelfcode:
            suppl += ' (%s)' % shelfcode
        elif kbx:
            suppl += ' (KBX%s)' % kbx
    elif shelfcode:
        suppl = shelfcode
    elif kbx:
        suppl = 'KBX%s' % kbx
    print 'writing', suppl, '...'
    sys.stdout.flush()
    if outfile is None:
        outfile = 'pinkdex.tex'
    fp = open(outfile, 'w')
    print >>fp, r'\def\dexname{%s}' % dexname
    if kbx:
        print >>fp, r'\def\Box{1}'
    if shelfcode or kbx:
        print >>fp, r'\def\Reverse{1}'
        print >>fp, r'\def\Shelf{1}'
    elif reverse:
        print >>fp, r'\def\Reverse{}'
    if suppl:
        print >>fp, r'\def\Supple{%s}' % suppl
        print >>fp, r'\def\Period{3}'
    else:
        print >>fp, r'\def\Period{0}'
    print >>fp, r'\input %s/dextex-current.tex' % mitsfs.TEXBASE

    if blob:
        print >>fp, blob
    letter = None

    for line in books:
        newletter = getattr(line, letterfield).upper()[0]
        if letter != newletter:
            if letter is not None and not suppl:
                print >>fp, r'\NextLetter'
            letter = newletter
            print letter,
            sys.stdout.flush()
        if shelfcode or kbx:
            if hassle:
                codes = ' [%s]' % str(line.codes).replace(':', r'\:')
            else:
                codes = ''
            if kbx is None:
                count = line.codes[shelfcode]
            else:
                # bleah
                count = sum([
                    line.codes[i]
                    for i in line.codes
                    if mitsfs.splitcode(i)[2] == str(kbx)])
            if inventory:
                inventory.add(line, count)

            print >>fp, '\\Book{%s}{%s}{%s} %% %s' % (
                mitsfs.texquote(titlecase(line.authortxt)),
                mitsfs.texquote(nicetitle(line)) + codes,
                count, str(line))
        else:
            print >>fp, book(line)
    print >>fp, r'\vfill \eject \bye'
    fp.close()
    print 'done.'


if (not inventory) or options.shelfcodes:
    args = []
    query = ''

    if options.predicate:
        query += ' and ' + options.predicate

    if options.boxdex is not None:
        print 'boxdex #', options.boxdex
        query += " and shelfcode like 'KBX%%' and doublecrap=%s"
        args.append(str(options.boxdex))
    elif options.shelfcodes:
        shelfqueries = []
        shelfargs = []
        for code in options.shelfcodes:
            at, shelfcode, doublecrap = mitsfs.splitcode(code)
            q = 'shelfcode = %s'
            shelfargs.append(shelfcode)
            if at:
                q += ' and book_series_visible'
            if doublecrap:
                q += ' and doublecrap=%s'
                shelfargs.append(doublecrap)
            shelfqueries.append('(' + q + ')')
        query += ' and (' + ' or '.join(shelfqueries) + ') '
        args += shelfargs
    else:
        query += " and position('KBX' in shelfcode) = 0"  # filter out KBXen

    if options.predicate:
        query += ' and ' + options.predicate

    if options.shelfcodes and False:
        books = mungeshelf(options.shelfcodes)
    else:
        query = (
            'select distinct title_id'
            ' from title natural join book natural join shelfcode'
            ' where not withdrawn' + query)
        books = mungedex(query, args, options.add)
        print 'sorting dex for pinkdex...',
        sys.stdout.flush()
        books.sort(key=lambda line: (line.placeauthor, line.placetitle))
        print 'done.'

    writedex(
        options.outfile, books,
        options.shelfcodes[0] if options.shelfcodes else None,
        options.suppl, options.hassle, kbx=options.boxdex)

    if not options.outfile and not options.shelfcodes and not options.boxdex:
        print 'sorting dex for titledex...',
        sys.stdout.flush()
        books.sort(key=lambda line: (line.placetitle, line.placeauthor))
        print 'done.'
        writedex(
            'titledex.tex', books, reverse=True, dexname='Titledex',
            letterfield='placetitle')

        print 'filtering for seriesdex...',
        books = [i for i in books if i.series]
        print 'done. (%d entries)' % len(books)
        print 'sorting for seriesdex...',
        books.sort(
            key=lambda line: (
                line.placeseries, line.placetitle, line.placeauthor))
        print 'done.'

        print 'generating serieslist...',
        sl = [r'\beginserieslist']
        for s in d.indices.series.iterkeys():
            dl = [mitsfs.DexLine(i) for i in d.indices.series[s]]
            sl.append(r'  \Series{%s}{%s}{%d}' % (
                mitsfs.texquote(s),
                mitsfs.texquote(
                    reduce(
                        lambda a, b: a and b,
                        # are all the authors the same?
                        [line.authors == dl[0].authors for line in dl]) and
                    dl[0].authortxt or 'authorship varies'),
                len(dl)
                ))
        sl.append(r'\endserieslist')
        print 'done'

        writedex(
            'seriesdex.tex', books, reverse=True, dexname='Seriesdex',
            letterfield='placeseries', blob='\n'.join(sl))
else:  # inventory
    timestart = time.time()
    print '; inventory', inventory.code, 'run begins', time.ctime(timestart)
    try:
        print 'computing', inventory.code, '...',
        sys.stdout.flush()
        inventory.compute()
        print 'done. (%.2f sec)' % (time.time() - timestart)
        codes = list(inventory.codes())
        print 'Generating shelfdexes for', ' '.join(codes)
        for shelfcode in codes:
            print
            print 'Generating shelfdex for', shelfcode
            divisions = list(d.cursor.execute(
                'select title_id'
                ' from shelf_divisions natural join shelfcode'
                '  natural join title_responsibility natural join entity '
                ' where inventory_id=%s and shelfcode=%s'
                '  and order_responsibility_by = 0 order by entity_name',
                (inventory.id, shelfcode)))
            if divisions:
                print 'Shelf divisions:'
                divisions = [mitsfs.Title(d, i) for i in divisions]
                for number, line in enumerate(divisions):
                    print line
                    divisions[number] = \
                        mitsfs.DexLine(line).shelfkey(shelfcode)
                slices = zip([None] + divisions, divisions + [None])

            shelf = list(progress_meter(inventory.shelf(shelfcode)))

            if not shelf:
                print 'empty shelfcode; punting'
                continue

            print 'sorting %s...' % shelfcode,
            sys.stdout.flush()
            shelf.sort(key=lambda tup: tup[0])
            print 'done.'

            slices = []
            if divisions:
                diviter = iter(divisions)
                terminus = diviter.next()
                print 'slicing:'
                for (coordinate, (key, line)) in enumerate(
                        progress_meter(shelf)):
                    if key > terminus:
                        slices.append(coordinate)
                        try:
                            terminus = diviter.next()
                        except StopIteration:
                            break

            if slices:
                slices = [('%s.%02d' % (shelfcode, count + 1), rng)
                          for (count, rng)
                          in enumerate(
                              zip([0] + slices, slices + [len(shelf)]))]
            else:
                slices = [(shelfcode, (0, len(shelf)))]

            slices = [(packet, (start, end))
                      for (packet, (start, end)) in slices
                      if start != end]

            for (packet, (start, end)) in slices:
                inventory.packet(packet, shelfcode)
                print 'shelfdex:', packet
                print start, shelf[start][1]
                print end, shelf[end - 1][1]

                outfile = 'shelfdex-%s.%s.tex' % (
                    inventory.code, packet.replace('/', '-'))

                writedex(
                    outfile, (shelf[i][1] for i in xrange(start, end)),
                    shelfcode, inventory.desc + ' ' + packet, None)
            print 'writing packet order'
            inventory.click()
    finally:
        timeend = time.time()
        print ';'
        print '; inventory', inventory.code, 'run ends', time.ctime(timeend)
        print '; duration %.2f seconds' % (timeend - timestart)
        print ';'
