#! /usr/bin/env python
# -*- coding: latin-1 -*-
# File: add2bib.py
'''
Add entry/entries to .bib file.
Default format produces citekeys like:
Schwilk+Isaac:2002 and Isaac+Schwilk+etal:2006.

:WARNING: works but currently *very* crude and rough!  (no special characters; macros alpha-lower only)
:author: Alan G Isaac
:contact: http://www.american.edu/cas/econ/faculty/isaac/isaac1.htm
:copyright: 2006 by Alan Isaac
:license: MIT (see `license.txt`_)
:date: 2006-09-25
:since: 2006-08-04
:change: 2006-09-24 move isbn to optional2 for books (not strictly correct, but often wanted)
:change: 2006-08-04 eliminated that final comma (believe illegal)
:change: 2006-08-24 add make_entry_citekey
:change: 2006-08-24 add label styles
:TODO: eliminate simpleparse dependency (via bibfile.BibEntry)
:TODO: fix make_entry_citekey to use one name + 'etal'? or just '+'?
:TODO: add checking for unique key
:TODO: allow multiple entries
:TODO: allow correcting entries
:TODO: use style file for HTML formatting
:TODO: check for macros against @string defs in .bib file
:TODO: macro handling (journals)
:TODO: add crossreferencing as option for inbook and incollection
:TODO: change default format to legal HTML names

.. _license.txt: ./license.txt
'''
__docformat__ = "restructuredtext en"
__authors__  =    ['Alan G. Isaac']
__version__ =    '0.3.1'
__needs__ = '2.4'


###################  IMPORTS  ##################################################
# import from standard library
#from string import ascii_lowercase
import os, shutil
import logging
logging.basicConfig(format='\n%(levelname)s:\n%(message)s\n')
add2bib_logger = logging.getLogger('bibstuff_logger')

# bibstuff imports
import bibfile
import bibstyles
################################################################################

entry_types = ("article","booklet","book","conference","inbook","incollection","inproceedings","manual","mastersthesis","misc","phdthesis","proceedings","techreport","unpublished")
valid_fields = dict(
)

"""
author or editor (Æ), author(A), address(a), booktitle(b), chapter(c), crossref(x), edition(e), editor(E), howpublished(h), institution(I), isbn(i), journal(j), key(k), month(m), note(z), number(n), organization(O), pages(p), publisher(P), school(S), series(s), title(T), type(t), url(u), volume(v), year(y), unused(fgl)

Currently will not provide the crossref field since crossrefs must be prepended. (Just warn?)
"""

article = dict(
required = 'ATjY',
optional1 = 'vnpm',
optional2 = 'z',
extras = 'kiu'
)
book = dict(
required = 'ÆTPY',
optional1 = 'ae',
optional2 = 'vnszi',
extras = 'kmu'
)
booklet = dict(
required = 'T',
optional1 = 'AhY',
optional2 = 'amz',
)
inbook = dict(
required = 'ÆTcpPY',
optional1 = 'a',
optional2 = 'vnsuemz',
extras = 'kiu'
)
incollection = dict(
required = 'ATbPY',
optional1 = 'Ecpa',
optional2 = 'vnsuemz',
extras = 'kiu'
)
inproceedings = dict(
required = 'ATbY',
optional1 = 'EpaOP',
optional2 = 'vnsmz',
extras = 'kiu'
)
manual = dict(
required = 'T',
optional1 = 'Oa',
optional2 = 'AemYz',
)
mastersthesis = dict(
required = 'ATSY',
optional1 = 'a',
optional2 = 'umz',
)
misc = dict(
required = '',
optional1 = 'AThY',
optional2 = 'mz',
)
phdthesis = dict(
required = 'ATSY',
optional1 = 'a',
optional2 = 'umz',
)
proceedings = dict(
required = 'TY',
optional1 = 'EaO',
optional2 = 'vnsmPz',
)
techreport = dict(
required = 'ATIY',
optional1 = 'tuna',
optional2 = 'mz',
)
unpublished = dict(
required = 'ATz',
optional1 = 'Y',
optional2 = 'm',
)

#for testing script:
#def raw_input(arg,stuff = ['','M. Me and Y. You', '2006', 'My Title', 'jtrix', '2', '3', 'jan', '99--100']): return stuff.pop(0)



def make_entry(choosetype='', options=False, extras=False):
	"""
	:author: Alan G. Isaac
	:date: 2006-08-11
	"""
	entry = bibfile.BibEntry()
	while not choosetype in entry_types:
		choosetype=raw_input("From\n"+", ".join(entry_types)+"\nchoose type: ")
	entry.entry_type = choosetype
	field_dict = eval(choosetype)
	#:TODO: test cite key against existing keys
	citekey = raw_input("Press return for autogenerated citekey.\nOr enter citekey (e.g., jones02aer): ")
	entry.citekey = citekey
	fields = field_dict['required'] + field_dict['optional1']
	if options or extras:
		fields = fields + field_dict['optional2']
	if extras:
		fields = fields + field_dict['extras']
	if 'A' in fields or 'Æ' in fields:
		entry['author'] = raw_input("author(s)? ").strip()
	if 'E' in fields or 'Æ' in fields:
		entry['editor'] = raw_input("editor(s)? ").strip()
	if 'Y' in fields:
		entry['year'] = raw_input("year? ").strip()
	if 'T' in fields:
		entry['title'] = raw_input("title? ").strip()
	if 'b' in fields:
		entry['booktitle'] = raw_input("booktitle? ").strip()
	if 'e' in fields:
		entry['edition'] = raw_input("edition? (E.g., 2nd)  ").strip()
	if 'c' in fields:
		entry['chapter'] = raw_input("chapter? ").strip()
	if 'j' in fields: #:TODO: journal key handling (all lower only?)
		entry['journal'] = raw_input("journal name? ").strip()
	if 'v' in fields:
		entry['volume'] = raw_input("volume? ").strip()
	if 'n' in fields:
		entry['number'] = raw_input("number? ").strip()
	if 'm' in fields: #:TODO: macro handling
		entry['month'] = raw_input("month? ").strip()
	if 'p' in fields:
		entry['pages'] = raw_input("pages? ").strip()
	if 'P' in fields:
		entry['publisher'] = raw_input("Publisher? ").strip()
	if 'a' in fields:
		entry['address'] = raw_input("address? ").strip()
	if 'h' in fields:
		entry['howpublished'] = raw_input("howpublished? ").strip()
	if 'I' in fields:
		entry['institution'] = raw_input("Institution? ").strip()
	if 'O' in fields:
		entry['organization'] = raw_input("Organization? ").strip()
	if 'S' in fields:
		entry['school'] = raw_input("School? ").strip()
	if 's' in fields:
		entry['series'] = raw_input("series? ").strip()
	if 't' in fields:
		entry['type'] = raw_input("type? (E.g., Working Paper) ").strip()
	if 'i' in fields:
		entry['isbn'] = raw_input("isbn? ").strip()
	if 'u' in fields:
		entry['url'] = raw_input("url? ").strip()
	if 'k' in fields:
		entry['key'] = raw_input("key (*not* citekey)? ").strip()
	if not citekey:
		citekey = make_entry_citekey(entry,[],label_style2)
		entry.citekey = citekey
	return entry


# for a discussion of name templates, see the NameFormatter docstring
# a style must always define a default entry_type
# use_max_names True -> first max_names names included, then etal
# use_max_names False -> first name included, then etal
label_style1 = dict(
name_template = 'v_|l{_}',
max_names = 2,
use_max_names = True,
name_name_sep = ('+','+'),
etal = 'etal',
anonymous = 'anon',
lower_name = False,
article = "%(names)s:%(year)s",
book = "%(names)s:%(year)s",
misc = "%(names)s:%(year)s",
default_type = "%(names)s:%(year)s",
)

#style2 shd be rst compatible
label_style2 = dict(
name_first = 'l{_}',
name_other = 'l{_}',
max_names = 2,
use_max_names = False,
name_name_sep = ('.','.'),
etal = '',
lower_name = True,
anonymous = 'anon',
article = "%(names)s-%(year)s-%(jrnl)s",
book = "%(names)s-%(year)s",
misc = "%(names)s-%(year)s",
default_type = "%(names)s-%(year)s",
)

#:note: this is a variant of a function in biblabel.py
#:TODO: make this a BibEntry method
#:TODO: integrate styles with CITATION_TEMPLATE styles (note: anon, lower_name, templates (names))
def make_entry_citekey(entry, used_citekeys,style=label_style1):
	"""return new entry key (as string)
	"""

	format_dict = {}
	entry_type = entry.entry_type.lower()
	try:
		label_template = style[entry_type]
	except KeyError:
		label_template = style['default_type']

	name_template = style['name_first']  #:TODO: ? adjust this ?
	max_names = style['max_names']
	name_name_sep = style['name_name_sep'][0] #:TODO: ? adjust this ?
	#name_parts_sep = style['name_parts_sep'] #superfluous if name templates used correctly; just in case ...
	lower_name = style['lower_name']
	etal = style['etal']

	#first, make names
	name_formatter = bibstyles.shared.NameFormatter(template = name_template)
	names_dicts = entry.get_names().get_names_dicts()
	#make list of 'v_|l' last names, which can possibly have multiple tokens (e.g., two piece last names)
	ls = [name_formatter.format_name(name_dict) for name_dict in names_dicts]
	if len(ls) > max_names:
		if use_max_names:
			ls = ls[:max_names] + [etal]
		else:
			ls = ls[0] + [etal]
	#for each name, join the tokens with an underscore (i.e., split on whitespace and then join with '_').
	#ls = [name_parts_sep.join( s.split() )  for s in ls]  #shd handle this with name template
	names =  name_name_sep.join(ls)
	if lower_name:
		names = names.lower()
	format_dict['names'] = names
	year = entry['year'] or '????'
	format_dict['year'] = year
	if entry_type == "article":
		jrnl = entry['journal']
		jrnl = ''.join(jrnl.split()).lower()  #keep macro; ow abbreviate (TODO: adjust this)
		jrnl = jrnl.replace("journal","j",1)
		format_dict['jrnl'] = jrnl  #short form, no spaces

	#make unique result: if needed, append suffix (sfx) b or c or d . . . to year
	sfx = ''; c = 1
	#while result+sfx in used_citekeys:
	while label_template%format_dict in used_citekeys:
		sfx = ascii_lowercase[c%26]*(1+c//26)  #:note: lowercase since BibTeX does not distinguish case
		format_dict['year'] = year+sfx
		c += 1
	result = label_template%format_dict

	return result

###########  HTML formatting  ########################
html_templates = dict(
journal = '''<p id='%(citekey)s' class='ref'>
<span class='author'>%(author)s</span>,
<span class='date'>%(year)s</span>,
&ldquo;<span class='title'>%(title)s</span>,&rdquo;
%(pubinfo)s.
</p>
''',
techreport = '''<p id='%(citekey)s' class='ref'>
<span class='author'>%(author)s</span>,
<span class='date'>%(year)s</span>,
&ldquo;<span class='title'>%(title)s</span>,&rdquo;
%(pubinfo)s.
</p>
''',
book = '''<p id='%(citekey)s' class='ref'>
<span class='author'>%(auted)s</span>, %(year)s,
<span class='booktitle'>%(booktitle)s</span>,
%(pubinfo)s.
</p>
''',
incollection = '''<p id='%(citekey)s' class='ref'>
<span class='author'>%(auted)s</span>, %(year)s,
<em>%(title)s</em>, in <span class='booktitle'>%(booktitle)s</span>,
%(pubinfo)s.
</p>
''',
)
text_templates = dict(
journal = '''%(author)s, %(year)s,
"%(title)s"
%(pubinfo)s.
''',
techreport = '''%(author)s, %(year)s,
"%(title)s"
%(pubinfo)s.
''',
book = '''%(auted)s, %(year)s,
%(booktitle)s
(%(address)s: %(publisher)s)
isbn: %(isbn)s
''',
incollection = '''%(author)s, %(year)s,
%(title)s, in %(booktitle)s,
%(pubinfo)s.
(%(address)s: %(publisher)s)
isbn: %(isbn)s
''',
)


def is_macro(s):
	"""Return bool,
	a crude guess if this is a macro.
	TODO: rethink this."""
	oneword = not (' ' in s )
	return oneword and (s.islower() or s.isupper())

def text_format(entry):
	from collections import defaultdict
	info = defaultdict(str)
	info.update(entry)
	entry_type = entry.entry_type.lower()
	add2bib_logger.info("entry_type = %s"%(entry_type))
	if entry_type == "article":
		info['journal'] = get_journal(entry)
		pubinfo = get_volnum(entry)
		if pages:
			pubinfo += pages
		info['pubinfo'] = pubinfo #TODO move into template
	if entry_type == "techreport":
		institution = entry['institution']
		type = entry['type']
		number = entry['number']
		pubinfo= "%s %s, %s"%(type, number, institution)
		info['pubinfo'] = pubinfo #TODO move into template
	elif entry_type in ["incollection","book"]:
		if entry_type == "book":
			info['booktitle'] = info['title']
			author = info['author']
			editor = info['editor']
			if author:
				auted = author
			elif editor:
				auted = editor + " (ed)"
			else:
				auted = "unknown"
			info['auted'] = auted
		else: #-> entry_type == "incollection":
			if info['editor']:
				info['auted'] = "%(author)s, in %(editor)s (ed)" % info
	result = text_templates[entry_type] % info
	return result
		
def get_journal(entry, jrnl_lst=None): #TODO: extract fr journal list
	"""Return string representation of journal,
	allowing opportunity to provide name to replace macro.
	TODO: automate macro substitution."""
	#if jrnlkey =~ "{.\+}", let journal=substitute(jrnlkey,'[{}]','','g')
	journal = entry['journal'] #might be a macro
	if is_macro(journal):
		if journal.islower():
			journal = journal.upper()
		jrnl = raw_input("Journal? (no braces) (Press enter to use '%s') "%(journal) )
		if jrnl:
			journal = jrnl
	return journal

def get_volnum(entry):
	if volume:
		volnum = str(volume)
		if number:
			volnum = str(volume) + "(" + str(number) + ")"
	elif number:
		volnume = str(number)
	else:
		volnum = ""
	return volnum

def get_pages(entry,dash='--',pagespref=('p. ','pp. ')):
	pages = entry['pages']
	if pages:
		if '--' in pages:
			pages = pagespref[1] + dash.join(pages.split("--"))
		elif '-' in pages:
			pages = pagespref[1] + dash.join("-".split(pages))
		else:
			pages = pagespref[0] + pages
	return pages

def html_format(entry):
	from collections import defaultdict
	entry_type = entry.entry_type.lower()
	info = defaultdict(str)
	info.update(entry)
	add2bib_logger.info("entry_type: %s"%(entry_type))
	#next we aggregate the publication information and apply formatting template
	if entry_type == "article":
		#journal may be a macro; ask to replace
		info['journal'] = get_journal(entry)
		pubinfo="<span class='journal'>" + journal + "</span>"
		volume = entry['volume']
		number = entry['number']
		volnum = get_volnum(entry)
		if volnum:
			pubinfo += " " + volnum
		pages = get_pages(entry,'&ndash;')
		if pages:
			pubinfo += pages
		#result = html_templates['journal']%dict(citekey=citekey,author=author,year=year,title=title,pubinfo=pubinfo)
		info['pubinfo'] = pubinfo
	if entry_type == "techreport":
		institution = entry['institution']
		type = entry['type']
		number = entry['number']
		pubinfo= "%s %s, %s"%(type, number, institution)
		info['pubinfo'] = pubinfo
	elif entry_type in ["incollection","book"]:
		if entry_type == "book":
			info['booktitle'] = info['title']
			author = info['author']
			editor = info['editor']
			if author:
				auted = author
			elif editor:
				auted = editor + " (ed)"
			else:
				auted = "unknown"
			info['auted'] = auted
		else: #-> entry_type == "incollection":
			if info['editor']:
				info['auted'] = "%(author)s, in %(editor)s (ed)"%(info)
				info['titleinfo']="<em>%(title)s</em>, in <span class='booktitle'>%(booktitle)s</span>,"%(entry)
		info['pubinfo']="(%(address)s: %(publisher)s)\nisbn: %(isbn)s"%(info)
	#apply template to aggregated information
	result = html_templates[entry_type] % info
	return result

		  

#-- Command line version of tool
def main():
	"""Command-line tool.
	See bibsearch.py -h for help.
	"""

	import sys
	import bibgrammar

	input = sys.stdin
	output = sys.stdout
	
	from optparse import OptionParser
	
	usage = """
	%prog [options]
	example: %prog -mt article -bo BIB_DATABASE
	"""


	parser = OptionParser(usage=usage, version ="%prog " + __version__)
	parser.add_option("-f", "--format", action="store",
	                  dest="format", default='b',
					  help="set format(s) of output\nb: BibTeX\nh: HTML\nt: text", metavar="FORMAT")
	parser.add_option("-m", "--more_fields", action="store_true",
					  dest="more_fields", default = False, help="input less common fields")
	parser.add_option("-M", "--MORE_FIELDS", action="store_true",
					  dest="MORE_FIELDS", default = False, help="input all relevant fields")
	parser.add_option("-v", "--verbose", action="store_true",
	                  dest="verbose", default=False,
					  help="Print INFO messages to stdout, default=%default")
	parser.add_option("-V", "--very_verbose", action="store_true",
	                  dest="very_verbose", default=False,
					  help="Print DEBUG messages to stdout, default=%default")
	parser.add_option("-t", "--type", action="store",
	                  dest="entry_type", default='',
					  help="set type of entry", metavar="ENTRYTYPE")
	parser.add_option("-o", "--outfile", action="store", type="string", dest="outfile",
					  help="Write formatted references to FILE", metavar="FILE")
	parser.add_option("-n", "--nuke", action="store_true", dest="overwrite", default=False,
					  help="CAUTION! silently overwrite outfile, default=%default")
	parser.add_option("-b", "--backup", action="store_true", dest="backup", default=False,
					  help="backup FILE to FILE.bak, default=%default")

	"""
	#TODO:
	parser.add_option("-I", "--ISBN", action="store", dest="ISBN", default=False,
					  help="use pyaws to add one entry by ISBN, default=%default")
	parser.add_option("-m", "--maxnames", action="store", type="int",
					  dest="maxnames",  default = 2, help="Max names to add to key")
	parser.add_option("-e", "--etal", action="store", type="string", \
					  dest="etal",  default = 'etal',help="What to add after max names")
	parser.add_option("-i", "--infile", action="store", type="string", dest="infile",
					  help="Parse FILE for citation references.", metavar="FILE")
	parser.add_option("-s", "--stylefile", action="store", dest="stylefile", default="default.py",
					  help="Specify user-chosen style file",metavar="FILE")
	"""

	# get options
	(options, args) = parser.parse_args()
	if options.verbose:
		add2bib_logger.setLevel(logging.INFO)
	if options.very_verbose:
		add2bib_logger.setLevel(logging.DEBUG)
	add2bib_logger.info("Script running.\nargs=%s"%(args))

	'''
	#TODO: error check cite keys, insert (v. append), sort
	# get database as text from .bib file(s) or stdin
	if len(args) > 0 :
		try :
		   src = ''.join(open(f).read() for f in args)
		except:
			print 'Error in filelist'
	else :
		src = sys.stdin.read()

	 
	bibfile_name = args[-1]
	if (os.path.splitext(bibfile_name)[-1]).lower() != ".bib":
		bib4txt_logger.warning(bibfile_name + " does not appear to be a .bib file")
	try :
		bibfile_as_string = open(bibfile_name,'r').read()
	except :
		print "Database file not found."
		sys.exit(1)

	# read input file (default: stdin)
	if options.infile:
		try:
			input = open(options.infile,'r')
		except:
			print "Cannot open: "+options.infile
			sys.exit(1)

	# create object to store parsed .bib file
	bibfile_processor = bibfile.BibFile()
	#store parsed .bib file in the bibfile_processor
	#  TODO: allow multiple .bib files
	bibgrammar.Parse(bibfile_as_string, bibfile_processor)

	bfile = bibfile.BibFile()
	bibgrammar.Parse(src, bfile)
	used_citekeys = [] # stores created keys
	'''

	entry = make_entry(options.entry_type, options.more_fields, options.MORE_FIELDS)

	# open output file for writing (default: stdout)
	if options.outfile:
		if options.backup and os.path.exists(options.outfile):
			shutil.copyfile(options.outfile, options.outfile+".bak")
		if options.overwrite or not os.path.exists(options.outfile):
			output = open(options.outfile,'w')
		else:
			add2bib_logger.info("Appending to %s.\n(Use -n option to nuke (overwrite) the old output file.)"
			                     %options.outfile)
			output = open(options.outfile,'a')
	output.write(str(entry))
	#print entry
	if 'h' in options.format:
		output.write( html_format(entry) )
	if 't' in options.format:
		output.write( text_format(entry) )
	output.close()

if __name__ == '__main__':
	main()

