# Copyright 2004,2005 Pierre Martineau <pmartino@users.sourceforge.net>
# This file is part of bibOOo, a python package to manipulate
# bibliography in an OpenOffice.org writer document.
#
# bibOOo is part of Bibus a free software;
# you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# bibOOo is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bibus; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA.
#
# this class implements Bibus style format

from bibOOo.bibOOoBase import *
import Format.Converter
import copy,sys
ALPHA = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']

class bibOOoPlus(bibOOoBase):
	"""This is the main class to interact with OOo using Bibus styles"""

# ------------------------ overloaded functions ------------------------------
	def __init__(self,bibStyle,con_type=1,host='localhost',port=8100,pipe='OOo_pipe',getRefFromId=None):
		"""We connect to the running OOo instance
		bibStyle is the style file
		getRefFromId is a funtion that returns a ref from the identifier inserted in OOo using the format
		getRefFromId(Id) = ('Identifier', 'BibiliographicType', 'Address', 'Annote', 'Author',
		 'Booktitle', 'Chapter', 'Edition', 'Editor', 'Howpublished', 'Institution',
		 'Journal', 'Month', 'Note', 'Number', 'Organizations', 'Pages', 'Publisher',
		 'School', 'Series', 'Title', 'Report_Type', 'Volume', 'Year', 'URL', 'Custom1',
		 'Custom2', 'Custom3', 'Custom4', 'Custom5', 'ISBN')
		 This is just needed for Author-date finalize formatting function.
		 """
		self.bibStyle = bibStyle	# this is the dict containing the style
		self.conv = self.__getConverter(self.bibStyle)	# converter to format fields
		self.getRefFromId = getRefFromId
		bibOOoBase.__init__(self,con_type,host,port,pipe)

	def connectToWriter(self, bibStyle = None, hilight = False, backColor = 0x00FFFF00, createBib = True, end = True):
		if bibStyle:
			self.bibStyle = bibStyle
			self.conv = self.__getConverter(self.bibStyle)	# converter to format fields
		bibOOoBase.connectToWriter(self,hilight,backColor,createBib,end)

	def __getConverter(self,style):
		"""Set the Converter or read from default
		Take into account the different versions of bibus styles"""
		if style:
			if style['version'] == 1.0:
				return Format.Converter.Converter(conv=style)
			else:
				return Format.Converter.Converter(conv=style['fields'])
		else:
			return Format.Converter.Converter()

	def setIndexFormat(self):
		bibOOoBase.setIndexFormat(self,self.bibStyle)

	def updateIndex(self):
		"""update the format of the index in addition to the fields and the index"""
		if self.bib: self.setIndexFormat()
		bibOOoBase.updateIndex(self)

	def _bibOOoBase__createBaseStyles(self):
		"""create base CharStyle for citations and index"""
		charStyles = self.stylesList.getByName('CharacterStyles')
		charstyle, charposition = self.bibStyle['citation']['base_style']
		if not charStyles.hasByName(bibOOo_cit_baseCharStyleName):
			self.createCharacterStyle(bibOOo_cit_baseCharStyleName,charstyle,charposition,'')
		else:
			self.updateCharacterStyle(bibOOo_cit_baseCharStyleName,charstyle,charposition,'')
		if not charStyles.hasByName(bibOOo_index_baseCharStyleName):
			self.createCharacterStyle(bibOOo_index_baseCharStyleName,bibOOo_regular,bibOOo_normal,'')

	def freezeCitations(self,messages = lambda i,x:sys.stdout.write('%s\n'%x),**kwds):
		"""**kwds is just for compatibility with the overloaded function in bibOOoBase"""
		if self.bibStyle['index']['IsNumberEntries']:
			cit_sort,cit_fuse,cit_range,cit_separator,cit_rangesep = self.bibStyle['citation']['numbering']
			bibOOoBase._bibOOoBase__freezeNumberedCitations(self,messages,cit_sort,cit_fuse,cit_range,cit_separator,cit_rangesep)
		else:
			self.__author_date(messages)					# new author-date format
		self.freezeIndex()							# we freeze the index
		self.tfm.dispose()							# we remove all the citations

	def finalize(self,messages = lambda i,x:sys.stdout.write('%s\n'%x) ):
		"""
		messages is to display formatting messages
		Default = print to stdout
		"""
		#self.updateRef()
		if self.bibStyle['index']['IsNumberEntries']:
			cit_sort,cit_fuse,cit_range,cit_separator,cit_rangesep = self.bibStyle['citation']['numbering']
			cit_order,cit_asc = False,False
		else:
			cit_fuse,cit_separator,cit_order,cit_asc = self.bibStyle['citation']['ad']
			cit_sort,cit_range = False,False
		bibOOoBase.finalize(self,messages = messages, cit_sort=cit_sort,cit_fuse=cit_fuse,cit_range=cit_range,cit_separator=cit_separator,cit_order=cit_order,cit_asc=cit_asc)

# ------------------------ new functions -------------------------------------

	def formatRef(self,ref=None,dbref=()):
		"""
		ref = bibOOo_Ref object. If = None => we create one
		dbref = a list or tuple = [identifier,BibliographicType,....,ISBN]
		return formatted ref using the current style
		"""
		if not ref: ref = self.createRef(dbref)		# we create a bibOOo_Ref if needed
		formattedRef = []
		# format from dbref
		if dbref:
			for field in BIB_FIELDS:
				try:
					func,param = self.conv[BIB_TYPE[dbref[1]]][field]
					tmpdata =  apply(func,(dbref[ BIBLIOGRAPHIC_FIELDS[field] ],self.conv['locale']) + param)
					formattedRef.append( tmpdata )
				except:
					print _("Because of an error, I didn't format reference type %(typeName)s and field %(fieldName)s") % {'typeName':BIB_TYPE[dbref[1]],'fieldName':field}
					formattedRef.append( dbref[ BIBLIOGRAPHIC_FIELDS[field] ] )
			if ref:
				ref.setRef(	formattedRef )			# we reformat the ref
			else:
				ref.createRef( formattedRef )		# we create a bibOOo_Ref if needed
		else:
			if not ref: ref = self.createRef()		# we create a bibOOo_Ref if needed
		return ref

	def formatRefFromId(self,Id):
		"""Create and format a reference corresponding at Identifier Id"""
		return self.formatRef(None,apply(self.getRefFromId,(Id,)) )

	def updateRef(self,messages = lambda x: sys.stdout.write("%s\n"%x)):
		"""Update all the cited references from the db"""
		notFormatted = False
		for ref in self:
			try:
				self.formatRef(ref,apply(self.getRefFromId,(ref.Identifier,)) )
			except:
				notFormatted = True
				apply(messages,(ref.Identifier,))
		if notFormatted:	# some refs were not formatted (not in db ?)
			apply(messages,(_("Some references were not formatted. Are you sure they are in the database?"),))

# ------------------------ Author-Date finalize ------------------------------


# author/Date formating and freezing

# duplicates
	def __nextLetter(self,letter):
		"""Increment the letter a->b->c ... ->z->aa->ab..."""
		if letter=='':
			return 'a'
		elif letter.endswith('z'):
			return "%sa"%self.__nextLetter(letter[:-1])
		else:
			return letter[:-1] + ALPHA[ALPHA.index(letter[-1])+1]

	def __duplicateAddLetter(self,duplicates,refs,cit):
		"""Duplicates are sorted in index order"""
		for i in duplicates:
			letter='a'
			for j in i:
				for k in j:
					refs[k].Year = refs[k].Year + letter
				letter = self.__nextLetter(letter)
		self.bib.update()									# update index to put the letter after the year
		return cit

	def __duplicateListUntilUnique(self,duplicates,refs,cit):
		"""Increase the number of authors until citations are uniques if possible"""
		format = self.bibStyle['citation']['ad_author']['format']
		fformat = lambda upto: self.__set_new_author_converter(self.bibStyle,format[:3] + \
							(( 'Format.Citation.Author.Joining.joining1',format[-1][1][:-2]+(upto,format[-1][1][-1]) ),) )
		for i in duplicates:
			previouslist = None
			tmpcitlist=[]
			upto=1
			while True:
				upto=upto+1
				previouslist = tmpcitlist[:]
				tmpcitlist = []
				for j in i:									# we loop until all citations are unique by increasing 'upto'
					tmpcit = self.__Citation(refs[j[0]],fformat(upto))
					if tmpcit in tmpcitlist:
						break
					else:
						tmpcitlist.append(tmpcit)
				if len(tmpcitlist) == len(i):				# succeed. All the citations have been formated and are different
					break
				elif previouslist == tmpcitlist: 			# if '==', means there is no more authors to differentiate and we failed
					print "Warning. The following references have the same citation key %s" %map(lambda x: refs[x[0]].Fields[0].Value,i)
					break
			# now we format the citation with the previously determined "upto" parameter
			tmpf = fformat(upto)
			for j in i:
				for k in j:
					cit[k] = self.__Citation(refs[k],tmpf)
		return cit

	def __noDuplicates(self,citations):
		"""If there is a duplicate in the list of citations, return False. Otherwise return True"""
		citations.sort()
		previous = citations[0]
		for i in xrange(len(citations[1:])):
			if previous == citations[i]:
				return False
			else:
				previous = citations[i]
		return True

	def	__duplicateNewAuthorFormat(self,duplicates,refs,cit):
		"""Setting a new author format to differentiate duplicates"""
		format = 3 * (self.bibStyle['citation']['ad_duplicates']['author']['format'],) + self.bibStyle['citation']['ad_author']['format'][-1:]
		newConv = self.__set_new_author_converter(self.bibStyle,format)
		for i in duplicates:
			citations=[]
			for j in i:
				for k in j:
					cit[k] = self.__Citation(refs[k],newConv)
				citations.append(cit[j[0]])
			if not self.__noDuplicates(citations):	# check now if it is unique
				print "Warning. The following references have the same citation key %s" %map(lambda x: refs[x[0]].Fields[0].Value,i)
		return cit

	def __duplicateAddField(self,duplicates,refs,cit):
		"""Add field to differentiate duplicates"""
		tmp=[]
		for i in self.bibStyle['citation']['ad_template']:	# we make a list of field's name. None if not a field
			if i[0] == 'field':
				tmp.append(i[1])
			else:
				tmp.append(None)
		pos1 = tmp.index(self.bibStyle['citation']['ad_duplicates']['field']['after'])	# position of the fields after which we must insert
		pos=0
		for i in tmp[:pos1+1]:
			if i == 'Author' or i == 'Editor':
				pos=pos+2	# +2 because of "et al"
			else:
				pos=pos+1
		for i in duplicates:
			citations=[]
			for j in i:
				for k in j:
					tmp=[]
					for l in self.bibStyle['citation']['ad_duplicates']['field']['add']:
						if l[0] == 'text':
							tmp.append(l[1:])
						else:
							tmp.append( (refs[k].Fields[OO_BIBLIOGRAPHIC_FIELDS[l[1]]].Value,l[2]) )
					cit[k] = cit[k][:pos] + tuple(tmp) + cit[k][pos:]
				citations.append(cit[j[0]])
			if not self.__noDuplicates(citations):	# check now if it is unique
				print "Warning. The following references have the same citation key %s" %map(lambda x: refs[x[0]].Fields[0].Value,i)
		return cit

# End of author/date duplicate formating

	def __Citation(self,ref,conv):
		"""Format citation using the converter conv
		return a tuple where each position is
		a tuple ('text',style)
		for author/editor => ('authors',style),('et al',style2)
		"""
		tmp=[]
		for field in self.bibStyle['citation']['ad_template']:
			if field[0] == 'text':
				tmp.append(field[1:])
			elif field[1] == 'Author':
				try:
					retref = apply(self.getRefFromId,(ref.Identifier,))
					if retref[OO_BIBLIOGRAPHIC_FIELDS['Author']]:
						func,param = conv[BIB_TYPE[int(retref[1])]][field[1]]
						auth,etal =  apply(func,(retref[OO_BIBLIOGRAPHIC_FIELDS[field[1]]],self.conv['locale'])+param)
						tmp.extend( ( (auth,field[2]) , (etal,self.bibStyle['citation']['ad_author']['etall_style']) ) )
					else:	# empty author
						anonymous = self.bibStyle['citation']['ad_author']['anonymous'][BIB_TYPE[retref[OO_BIBLIOGRAPHIC_FIELDS['BibiliographicType']]]]
						if anonymous['type'] == 2:	# 0 = Nothing / 1 = Field / 2 = string
							tmp.append( (anonymous['string'],field[2]) )
						elif anonymous['type'] == 1:
							tmp.append( (getattr(ref,anonymous['field']) , field[2]) )
						else:
							pass
				except IndexError:					# the ref is not in the database, we use the Author field in OOo
					tmp.append( (ref.Author , field[2]) )
			#
			elif field[1] == 'Editor':
				try:
					retref = apply(self.getRefFromId,(ref.Identifier,))
					func,param = conv[BIB_TYPE[int(retref[1])]][field[1]]
					auth,etal =  apply(func,(retref[OO_BIBLIOGRAPHIC_FIELDS[field[1]]],self.conv['locale'])+param)
					tmp.extend( ( (auth,field[2]) , (etal,self.bibStyle['citation']['ad_author']['etall_style']) ) )
				except IndexError:					# the ref is not in the database, we use the Author field in OOo
					tmp.append( (ref.Editor , field[2]) )
			#
			else:
				tmp.append((getattr(ref,field[1]),field[2]))
		return tuple(tmp)

	def __set_new_author_converter(self,dico,format):
		# setting to current value
		if dico:
			if dico['version'] == 1.0:
				tmp_format_dico = copy.deepcopy(dico)
			else:
				tmp_format_dico = copy.deepcopy(dico['fields'])
		else:
			tmp_format_dico = Format.Converter.def_conv
		# changing authors citations according to current citation format
		# citation converters
		for typ in BIB_TYPE:
			tmp_format_dico[typ]['Author'] = format
			tmp_format_dico[typ]['Editor'] = format
		return Format.Converter.Converter(conv=tmp_format_dico)

	def __set_cit_converter(self):
		# setting converters for first and next author citations
		# changing authors citations according to current citation format
		format = self.bibStyle['citation']['ad_author']['format']
		listall,upto = self.bibStyle['citation']['ad_author']['listall'] # list all author on first occurence, if <= upto
		# citation converters (next and first citation formats)
		cit_next_conv = self.__set_new_author_converter(self.bibStyle,format)
		if listall:
			format=format[:3] + (( format[-1][0],format[-1][1][:-2]+(upto,format[-1][1][-1]) ),)
		cit_first_conv = self.__set_new_author_converter(self.bibStyle,format)
		return 	cit_first_conv,cit_next_conv

	def __insertCitationAD(self,cit,where):
		"""cit is a tuple (('test',style),('text2',style),...). where is the anchor where we want to insert"""
		bb = self.tfm.BracketBefore
		ba = self.tfm.BracketAfter
		c = where.Text.createTextCursorByRange(where)					# cursor
		c.Text.insertString(c,bb,True)
		c.CharStyleName = bibOOo_cit_baseCharStyleName
		c.setPropertiesToDefault( ('CharCaseMap','CharPosture','CharUnderline','CharWeight') )
		basePosture,baseWeight,baseCaps,baseUnderline = c.CharPosture,c.CharWeight,c.CharCaseMap,c.CharUnderline # base values
		for citation,style in cit:
			if citation:				# if citation == '', do nothing or CharStyle changing will format previous characters
				# setting specific attributes if and only if different from bibus_citation_base
				c = where.Text.createTextCursorByRange(c.End)
				c.Text.insertString(c,citation,True)			# we insert in the text
				c.CharStyleName = bibOOo_cit_baseCharStyleName
				c.setPropertiesToDefault( ('CharCaseMap','CharPosture','CharUnderline','CharWeight') )
				if style != bibOOo_base:
					if style & bibOOo_italic and basePosture != ITALIC: c.CharPosture = ITALIC
					elif not style & bibOOo_italic and basePosture == ITALIC: c.CharPosture = FontSlantNone
					#
					if style & bibOOo_bold and baseWeight != BOLD: c.CharWeight = BOLD
					elif not style & bibOOo_bold and baseWeight == BOLD: c.CharWeight = NORMAL
					#
					if style & bibOOo_caps and baseCaps != UPPERCASE: c.CharCaseMap = UPPERCASE
					elif style & bibOOo_smallcaps and baseCaps != SMALLCAPS: c.CharCaseMap = SMALLCAPS
					elif not style & bibOOo_caps and not style & bibOOo_smallcaps and baseCaps!=CaseMapNone: c.CharCaseMap = CaseMapNone
					#
					if style & bibOOo_underline and baseUnderline != SINGLE: c.CharUnderline = SINGLE
					elif not style & bibOOo_underline and baseUnderline == SINGLE: c.CharUnderline = FontUnderlineNone
		#
		c = where.Text.createTextCursorByRange(c.End)
		c.Text.insertString(c,ba,True)
		c.CharStyleName = bibOOo_cit_baseCharStyleName
		c.setPropertiesToDefault( ('CharCaseMap','CharPosture','CharUnderline','CharWeight') )

	def __author_date(self,messages = lambda i,x:sys.stdout.write('%s\n'%x)):
		refs=self.getCitations(order='document')# refs in the text order
		refsIndex = None						# references cited and ordered as in index
		cit_first_conv,cit_next_conv = self.__set_cit_converter()	# fields formating for first and next citations
		cit=[]									# list of citation values, same order than refs
		cit_next={}								# citation if not first apparition. Needed for letter after year.
		idlist=[]								# list of citation identifier, same order than refs
		alreadyCited=[]							# list of identifier already cited. Used to know if it is the first citation
		apply(messages, (.5,msg8) )
		for i in xrange(len(refs)):
			idlist.append( refs[i].Identifier )
			if idlist[i] not in alreadyCited:
				cit.append( self.__Citation(refs[i],cit_first_conv) )	# value of the first in-text Citation
				cit_next[i] = self.__Citation(refs[i],cit_next_conv)
				alreadyCited.append(idlist[i])
			else:
				cit.append( self.__Citation(refs[i],cit_next_conv) )	# value of the second and more in-text Citation
		# if we add letter after year. We modify cit by replacing first citation by citation_next if it is a duplicate
		if self.bibStyle['citation']['ad_duplicates']['type'] == 0:
			for i in cit_next:
				if cit_next[i] in cit: cit[i] = cit_next[i]
		# group duplicates in cit. For each key in group_cit: i of ref in refs with identical cit in document order
		apply(messages, (.6,msg9) )
		group_cit={}
		for i in xrange(len(cit)):
			if group_cit.has_key(cit[i]):
				group_cit[cit[i]].append(i)
			else:
				group_cit[cit[i]] = [i]
		# look if duplicates correspond to the same id or not
		# we group for each id
		# result = [[1,4,[2,3]],[...],...]
		# means ref # 1,4,2,3 are duplicates and 2,3 correspond to the same id
		duplicate = [False]*len(refs)
		tmp4=[]
		for key in group_cit.keys():
			tmp3=[]
			tmp=[(idlist[i],i) for i in group_cit[key]]
			tmp.sort()
			if tmp[0][0] != tmp[-1][0]:
				# the i in tmp are duplicates since at least 2 different id
				tmp2,tmp3=[],[]
				ident0=tmp[0][0]
				for ident,i in tmp:
					if ident == ident0:
						tmp2.append(i)
					else:
						ident0 = ident
						tmp3.append(tmp2)
						tmp2=[i]
				tmp3.append(tmp2)
			if tmp3: tmp4.append(tmp3)
		# duplicates
		if tmp4:
			apply(messages, (.65,msg10 %len(tmp4)) )
			# add a letter
			if self.bibStyle['citation']['ad_duplicates']['type'] == 0:			# 0 = add a letter; 1 = change author ; 2 = add field
				refsIndex = self.getCitations(order='index')							# letter after year must be added in the index order
				idindex={}															# idindex[id]=position in index
				for i in xrange(len(refsIndex)):
					idindex[ refsIndex[i].Identifier ] = i
				#del refsIndex
				tmp=[]
				for tmp3 in tmp4:
					tmp2 = [(idindex[idlist[x[0]]],x) for x in tmp3]
					tmp2.sort()														# sorted by index order
					tmp.append( [x[1] for x in tmp2] )
				self.__duplicateAddLetter(tmp,refs,cit)								# add the correct letter after the year
				# re-format the citations
				cit=[]
				alreadyCited=[]							# list of identifier already cited. Used to know if it is the first citation
				for i in xrange(len(refs)):
					if idlist[i] not in alreadyCited:
						cit.append( self.__Citation(refs[i],cit_first_conv) )	# value of the first in-text Citation
						alreadyCited.append(idlist[i])
					else:
						cit.append( self.__Citation(refs[i],cit_next_conv) )	# value of the second and more in-text Citation
			#
			# Change author format
			elif self.bibStyle['citation']['ad_duplicates']['type'] == 1:
				if self.bibStyle['citation']['ad_duplicates']['author']['type'] == 0:
					cit = self.__duplicateListUntilUnique(tmp4,refs,cit)
				else:
					cit = self.__duplicateNewAuthorFormat(tmp4,refs,cit)
			#
			# Add field
			elif self.bibStyle['citation']['ad_duplicates']['type'] == 2:
				cit = self.__duplicateAddField(tmp4,refs,cit)
			else:
				print "What are we doing here! bibOOo.py line 585"
		#
		# we now insert the formatted citations in the text
		fuse, sep, how, asc = self.bibStyle['citation']['ad']					# fuse,separator,order,asc
		if fuse:
			apply(messages, (.7,msg11) )
			refs2 = self.groupCitations(refs)							# group citations
			if how == 'index':
				if not refsIndex: refsIndex = self.getCitations(order='index')
				refsIndexl = [ref.Identifier for ref in refsIndex]
			apply(messages, (.8,msg12 %len(refs2)) )
			for reflist in refs2:
				if how == 'index':
					reflist.sort(lambda x,y: self.cmpRefs(x.Identifier,y.Identifier,refsIndexl))	# sorting citation by index order
					if not asc: reflist.reverse()													# we must be compatible with python2.3
				elif how in BIB_FIELDS:
					reflist.sort(lambda x,y: self.cmpRefsByField(x,y,how))					# sorting by field 'how' order
					if not asc: reflist.reverse()
				tmp = map(lambda x: cit[refs.index(x)], reflist)
				#tmp.sort()
				tmp = reduce(lambda x,y:x+((sep,bibOOo_base),)+y, tmp )
				tmpcursor = reflist[0].Anchor.Text.createTextCursorByRange(reflist[0].Anchor)
				tmpcursor.gotoRange(reflist[-1].Anchor,True)
				self.__insertCitationAD(tmp,tmpcursor)
		else:
			apply(messages, (.8,msg12 %len(refs)) )
			for i in xrange(len(refs)):
				self.__insertCitationAD(cit[i],refs[i].Anchor)
		return
		
	def cmpRefs(self,ref1,ref2,reflist):
		i1 = reflist.index(ref1)
		i2 = reflist.index(ref2)
		if i1 < i2: return -1
		elif i1 > i2: return 1
		else: return 0
		
	def cmpRefsByField(self,ref1,ref2,field):
		if getattr(ref1,field) < getattr(ref2,field): return -1
		elif getattr(ref1,field) > getattr(ref2,field): return 1
		else: return 0
#
# end of author-date formating
#












