#!/usr/bin/python

import os
import string
import re
import getopt
import sys
import __main__

outdir = 'out'
initfile = ''
program_version = '1.2.2'

cwd = os.getcwd ()
include_path = [cwd]

# TODO: use splitting iso. \mudelagraphic.

#
default_music_fontsize = 16
default_text_fontsize = 12

# latex linewidths:
# indices are no. of columns, papersize,  fontsize
# Why can't this be calculated?
latex_linewidths = {
 1: {'a4':{10: 345, 11: 360, 12: 390},
	 'a5':{10: 276, 11: 276, 12: 276},
	 'b5':{10: 345, 11: 356, 12: 356},
	 'letter':{10: 345, 11: 360, 12: 390},
	 'legal': {10: 345, 11: 360, 12: 390},
	 'executive':{10: 345, 11: 360, 12: 379}},
 2: {'a4':{10: 167, 11: 175, 12: 190},
	 'a5':{10: 133, 11: 133, 12: 133},
	 'b5':{10: 167, 11: 173, 12: 173},
	 'letter':{10: 167, 11: 175, 12: 190},
	 'legal':{10: 167, 11: 175, 12: 190},
	 'executive':{10: 167, 11: 175, 12: 184}}}


options = [
  ('', 'h', 'help', 'print help'),
  ('EXT', 'f', 'format', 'set format.  EXT is one of texi and latex.'),
  ('', 'v', 'version', 'print version information' ),
  ('FILE', 'o', 'outname', 'prefix for filenames'),
  ('DIM',  '', 'default-mudela-fontsize', 'default fontsize for music.  DIM is assumed to in points'),
#  ('DIM', '', 'force-mudela-fontsize', 'force fontsize for all inline mudela. DIM is assumed to in points'),
  ('', '', 'force-verbatim', 'make all mudela verbatim'),
  ('', 'M', 'dependencies', 'write dependencies'),
  ('DIR', 'I', 'include', 'include path'),
  ('', '', 'init', 'mudela-book initfile')
  ]

format = 'latex'
no_match = 'a\ba'

# format specific strings, ie. regex-es for input, and % strings for output
re_dict = {
	'latex': {'input': '\\\\input{?([^}\t \n}]*)',
		  'include': '\\\\include{([^}]+)}',
		  'include-mudela':r"""\begin%s{mudela}
%s
\end{mudela}""",
		  'header': r"""\\documentclass(\[.*?\])?""",
		  'preamble-end': '\\\\begin{document}',
		  'verbatim': r"""(?s)\\begin{verbatim}(.*?)\\end{verbatim}""",
		  'verb': r"""\\verb(.)(.*?)\1""",
		  'mudela-file': '\\\\mudelafile(\[[^\\]]+\])?{([^}]+)}',
		  'mudela' : '\\\\mudela(\[.*?\])?{(.*?)}',
		  'mudela-block': r"""(?s)\\begin(\[.*?\])?{mudela}(.*?)\\end{mudela}""",
		  'interesting-cs': '\\\\(chapter|section|mudelagraphic|twocolumn|onecolumn)',
		  'quote-verbatim': r"""\begin{verbatim}%s\end{verbatim}""",
		  'def-post-re': r"""\\def\\postMudelaExample""",
		  'def-pre-re': r"""\\def\\preMudelaExample""",		  
		  'default-post': r"""\def\postMudelaExample{}""",
		  'default-pre': r"""\def\preMudelaExample{}""",
		  'output-eps': '\\noindent\\parbox{\\mudelaepswidth{%s.eps}}{\includegraphics{%s.eps}}',
		  'output-tex': '\\preMudelaExample \\input %s.tex \\postMudelaExample\n'
		  },
	'texi': {'input': '@include[ \n\t]+([^\t \n]*)',
		 'include': no_match,
		 'include-mudela': """@mudela[%s]
%s
@end mudela
""",
		 'header': no_match,
		 'preamble-end': no_match,
		 'verbatim': r"""(?s)@example(.*?)@end example$""",
		 'verb': r"""@code{(.*?)}""",
		 'mudela-file': '@mudelafile(\[[^\\]]+\])?{([^}]+)}',
		 'mudela' : '@mudela(\[.*?\])?{(.*?)}',
		 'mudela-block': r"""(?s)@mudela(\[.*?\])?(.*?)@end mudela""",
		 'interesting-cs': r"""[\\@](node|mudelagraphic)""",
		 'quote-verbatim': r"""@example
%s
@end example""",
		 'output-all': r"""@tex
\input %s.tex
@end tex
@html
<img src=%s.png>
@end html
""",
		}
	}




def get_re (name):
	return  re_dict[format][name]


def bounding_box_dimensions(fname):
	try:
		fd = open(fname)
	except IOError:
		error ("Error opening `%s'" % fname)
	str = fd.read (-1)
	s = re.search('%%BoundingBox: ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)', str)
	if s:
		return (int(s.group(3))-int(s.group(1)), 
			int(s.group(4))-int(s.group(2)))
	else:
		return (0,0)


def find_file (name):
	for a in include_path:
		try:
			nm = os.path.join (a, name)
			f = open (nm)
			return nm
		except IOError:
			pass
	return ''

def error (str):
	sys.stderr.write (str + "\n  Exiting ... \n\n")
	raise 'Exiting.'


def compose_full_body (body, opts):
	"Construct the text of an input file: add stuff to BODY using OPTS as options."
	paper = 'a4'
	music_size = default_music_fontsize
	latex_size = default_text_fontsize

	cols = 1
	for o in opts:
		m = re.search ('^(.*)paper$', o)
		if m:
			paper = m.group (1)
		

		m = re.match ('([0-9]+)pt', o)
		if m:
			music_size = string.atoi(m.group (1))

		m = re.match ('latexfontsize=([0-9]+)pt', o)
		if m:
			latex_size = string.atoi (m.group (1))


	if 'twocolumn' in opts:
		cols = 2
		
	if 'fragment' or 'singleline' in opts:
		l = -1.0;
	else:
		l = latex_linewidths[cols][paper][latex_size]

	if not 'nofly' in opts and not re.search ('\\\\score', body):
		opts.append ('fly')


	if 'fly' in opts:
		body = r"""\score { 
  \notes\relative c {
    %s
  }
  \paper { }  
}""" % body

		
	body = r"""
%% Generated by mudela-book.py
\include "paper%d.ly"
\paper  { linewidth = %f \pt; } 
""" % (music_size, l) + body

	return body


#
# Petr, ik zou willen dat ik iets zinvoller deed,
# maar wat ik kan ik doen, het verandert toch niets?
#   --hwn 20/aug/99
#


def read_tex_file (filename):
	"""Read the input file, substituting for \input, \include, \mudela{} and \mudelafile"""
	str = ''
	for fn in [filename, filename+'.tex', filename+'.doc']:
		try:
			f = open(fn)
			str = f.read (-1)
		except:
			pass
		

	if not str:
		raise IOError

	retdeps =  [filename]

	def inclusion_func (match, surround):
		insert = match.group (0)
		try:
			(insert, d) = read_tex_file (match.group(1))
			deps = deps + d
			insert = surround + insert + surround
		except:
			sys.stderr.write("warning: can't find %s, let's hope latex will\n" % m.group(1))

		return (insert, deps)
	
	def include_func (match, d = retdeps):
		(s,d) = inclusion_func (match, '\\newpage ', retdeps)
		retdeps = retdeps + d
		return s

	str = re.sub (get_re ('input'), include_func, str)

	def input_func (match, d = retdeps):
		(s,d) = inclusion_func (match, '', retdeps)
		retdeps = retdeps + d
		return s

	str = re.sub (get_re ('include'), input_func, str)
	
	return (str, retdeps)

def scan_preamble (str):
	options = []
	m = re.search (get_re ('header'), str)

	# should extract paper & fontsz.
	if m and m.group (1):
		options = options + re.split (',[\n \t]*', m.group(1)[1:-1])

	def verbose_fontsize ( x):
		if o.match('[0-9]+pt'):
			return 'latexfontsize=' + x
		else:
			return x 
			
	options = map (verbose_fontsize, options)

	return options


def completize_preamble (str):
	m = re.search (get_re ('preamble-end'), str)
	if not m:
		return str
	
	preamble = str [:m.start (0)]
	str = str [m.start(0):]
	
	if not re.search (get_re('def-post-re'), preamble):
		preamble = preamble + get_re('default-post')
	if not re.search (get_re ('def-pre-re'),  preamble):
		preamble = preamble + get_re ('default-pre')

	if  re.search ('\\\\includegraphics', str) and not re.search ('usepackage{graphics}',str):
		preamble = preamble + '\\usepackage{graphics}\n'

	return preamble + str
	
def find_mudela_sections (str):
	"""Find mudela blocks, while watching for verbatim. Returns
	(STR,MUDS) with \mudelagraphic substituted for the blocks in STR,
	and the blocks themselves MUDS"""
	
	mudelas = []
	verbblocks = []
	noverbblocks = []

	while str:
		m = re.search (get_re ('verbatim'), str)
		m2 = re.search (get_re ("verb"), str)

		if  m == None and m2 == None:
			noverbblocks.append (str)
			str = ''
			break

		if m == None:
			m = m2

		if m2 and m2.start (0) < m.start (0):
			m = m2
			
		noverbblocks.append (str[:m.start (0)])
		verbblocks.append (m.group (0))
		str = str [m.end(0):]

	def mudela_short (match):
		"Find \mudela{}, and substitute appropriate \begin / \end blocks."
		opts = match.group (1)
		if opts:
			opts = ',' + opts[1:-1]
		else:
			opts = ''
		return r"""\begin[eps,fragment%s]{mudela}
  \context Staff <
    \context Voice{
      %s
    }
  >
\end{mudela}""" % (opts, match.group (2))

	def mudela_file (match):
		"Find \mudelafile, and substitute appropriate \begin / \end blocks."
		d = [] #, d = retdeps
		full_path = find_file (match.group (2))
		if not full_path:
			error("error: can't find file `%s'\n" % match.group(2))

		d.append (full_path)
		f = open (full_path)
		str = f.read (-1)
		opts = match.group (1)
		if opts:
			opts = re.split (',[ \n\t]*', opts[1:-1])
		else:
			opts = []

		if re.search ('.fly$', full_path):
			opts.append ('fly')
		elif re.search ('.sly$', full_path):
			opts = opts + [ 'fly','fragment']
		elif re.search ('.ly$', full_path):
			opts .append ('nofly')
			
		str_opts = string.join (opts, ',')
		if str_opts: str_opts = '[' + str_opts + ']'


		str = "%% copied from %s\n" % full_path + str 
		return get_re ('include-mudela') % (str_opts, str)


	def find_one_mudela_block (match,muds =mudelas):
		"extract body and options from a mudela block, and append into MUDELAS"
		opts = match.group (1)
		if opts:
			opts = opts[1:-1]
		else:
			opts = ''
			
		body = match.group (2)
		optlist = re.split (', *', opts)
		muds.append ((body, optlist))

		return '\\mudelagraphic\n'#  UGH.

	doneblocks = []
	for b in noverbblocks:
		b = re.sub (get_re('mudela-file'),  mudela_file, b)
		b = re.sub (get_re('mudela'), mudela_short, b)
		b = re.sub (get_re ("mudela-block"),  find_one_mudela_block, b)
		doneblocks.append (b)

	allblocks = []
	verbblocks.append ('')
	while doneblocks:
		allblocks = allblocks + doneblocks[0:1] +  verbblocks[0:1]
		verbblocks = verbblocks[1:]
		doneblocks =doneblocks[1:]

	str = string.join (allblocks,'')

	return (str, mudelas)


def eps_file_cs (base):
	if format == 'latex':
		return
	els

def tex_file_cs (base):
	return 




def make_files (str, mudelas, filename):	
	(chapter, section, count) = (0,0,0)
	total = 0
	done = ''

	# number them.
	numbering = []
	current_opts = []
	while str:
		m = re.search (get_re ('interesting-cs'), str)
		if not m:
			done = done + str
			str = ''
			break
		
		done = done + str[:m.end (0)]
		str = str[m.end(0):]
		g = m.group (1)

		if g == 'twocolumn':
			current_opts.append ('twocolumn')
		elif g  == 'onecolumn':
			try:
				current_opts.remove ('twocolumn')
			except IndexError:
				pass
		if g == 'mudelagraphic':
			numbering.append ((chapter, section, count, current_opts[:]))
			count = count + 1
		elif g == 'chapter':
			(chapter, section, count)  = (chapter + 1, 0, 0)
		elif g == 'section' or g == 'node':
			(section, count)  = (section + 1, 0)
			

	todo = []
	str = done
	

	done = ''
	while str:
		m = re.search ('\\\\mudelagraphic', str)
		if not m:
			done = done + str;
			str = ''
			break

		done = done + str[:m.start(0)]
		str = str[m.end(0):]
		
		(c1,c2,c3, file_opts) = numbering[0]
		(body, opts) = mudelas[0]
		numbering = numbering[1:]
		mudelas = mudelas[1:]
		
		opts = opts + file_opts

		base = '%s-%d.%d.%d'  % (filename, c1, c2,c3)
		if 'verbatim' in opts:
			done = done + get_re ('quote-verbatim') % body
			

		body = compose_full_body (body, opts)
		updated = update_file (body, base + '.ly')
		def is_updated (extension, t = todo):
			for x in t:
				if t[0] == extension:
					return 1
			return 0

		if not os.path.isfile (base + '.tex') or updated:
			todo.append (('tex', base, opts))
			updated = 1

		for o in opts:
			m = re.search ('intertext="(.*?)"', o)
			if m:
				done = done  + m.group (1)

		if format == 'texi':
			opts.append ('png')
			
		if 'png' in opts:
			opts.append ('eps')

		if 'eps' in opts and (is_updated ('tex') or
				      not os.path.isfile (base + '.eps')):
			todo.append (('eps', base, opts))

		if 'png' in opts and (is_updated ('eps') or
				      not os.path.isfile (base + '.png')):
			todo.append (('png', base, opts))
			
		if format == 'latex':
			if 'eps' in opts :
				done = done + get_re ('output-eps') %  (base, base )
			else:
				done = done + get_re ('output-tex') % base
		elif format == 'texi':
			done = done + get_re ('output-all') % (base, base) 


	compile_all_files (todo)

	def find_eps_dims (match):
		"Fill in dimensions of EPS files."
		fn =match.group (1)
		dims = bounding_box_dimensions (fn)
		
		return '%ipt' % dims[0]

	done = re.sub (r"""\\mudelaepswidth{(.*?)}""", find_eps_dims, done)

	return done 

def system (cmd):
	sys.stderr.write ("invoking `%s'\n" % cmd)
	st = os.system (cmd)
	if st:
		sys.stderr.write ('Error command exited with value %d\n' % st)
	return st

def compile_all_files ( list):
	eps = []
	tex = []
	gif = []
	for l in list:
		if l[0] == 'eps':
			eps.append (l[1])
		elif l[0] == 'tex':
			tex.append (l[1] + '.ly')
		elif l[0] == 'png':
			gif.append (l[1])

	if tex:
		lilyopts = map (lambda x:  '-I ' + x, include_path)
		texfiles = string.join (tex, ' ')
		lilyopts = string.join (lilyopts, ' ' )
		system ('lilypond %s %s' % (lilyopts, texfiles))


	for e in eps:
		cmd = r"""tex %s; dvips -E -o %s %s""" % \
		      (e, e + '.eps', e)
		system (cmd)

	for g in gif:
		cmd = r"""gs -sDEVICE=pgm  -dTextAlphaBits=4 -dGraphicsAlphaBits=4  -q -sOutputFile=- -r90 -dNOPAUSE %s -c quit | pnmcrop | pnmtopng > %s"""

		cmd = cmd % (g + '.eps', g + '.png')
		system (cmd)

	
def update_file (body, name):
	same = 0
	try:
		f = open (name)
		fs = f.read (-1)
		same = (fs == body)
	except:
		pass

	if not same:
		f = open (name , 'w')
		f.write (body)
		f.close ()

	
	return not same



def getopt_args (opts):
	"Construct arguments (LONG, SHORT) for getopt from  list of options."
	short = ''
	long = []
	for o in opts:
		if o[1]:
			short = short + o[1]
			if o[0]:
				short = short + ':'
		if o[2]:
			l = o[2]
			if o[0]:
				l = l + '='
			long.append (l)
	return (short, long)

def option_help_str (o):
	"Transform one option description (4-tuple ) into neatly formatted string"
	sh = '  '	
	if o[1]:
		sh = '-%s' % o[1]

	sep = ' '
	if o[1] and o[2]:
		sep = ','
		
	long = ''
	if o[2]:
		long= '--%s' % o[2]

	arg = ''
	if o[0]:
		if o[2]:
			arg = '='
		arg = arg + o[0]
	return '  ' + sh + sep + long + arg


def options_help_str (opts):
	"Transform a list of options into a neatly formatted string"
	w = 0
	strs =[]
	helps = []
	for o in opts:
		s = option_help_str (o)
		strs.append ((s, o[3]))
		if len (s) > w:
			w = len (s)

	str = ''
	for s in strs:
		str = str + '%s%s%s\n' % (s[0], ' ' * (w - len(s[0])  + 3), s[1])
	return str

def help():
	sys.stdout.write("""Usage: mudela-book [options] FILE\n
Generate hybrid LaTeX input from Latex + mudela
Options:
""")
	sys.stdout.write (options_help_str (options))
	sys.stdout.write (r"""Warning all output is written in the CURRENT directory



Report bugs to bug-gnu-music@gnu.org.

Written by Tom Cato Amundsen <tomcato@xoommail.com> and
Han-Wen Nienhuys <hanwen@cs.uu.nl>
""")

	sys.exit (0)


def write_deps (fn, target,  deps):
	sys.stdout.write('writing `%s\'\n' % fn)

	f = open (fn, 'w')
	
	target = target + '.latex'
	f.write ('%s: %s\n'% (target, string.join (deps, ' ')))
	f.close ()

		
def identify():
	sys.stdout.write ('mudela-book (GNU LilyPond) %s\n' % program_version)

def print_version ():
	identify()
	sys.stdout.write (r"""Copyright 1998--1999
Distributed under terms of the GNU General Public License. It comes with
NO WARRANTY.
""")


def main():
	global outdir, initfile, defined_mudela_cmd, defined_mudela_cmd_re
	outname = ''
	try:
		(sh, long) = getopt_args (__main__.options)
		(options, files) = getopt.getopt(sys.argv[1:], sh, long)
	except getopt.error, msg:
		sys.stderr.write("error: %s" % msg)
		sys.exit(1)

	do_deps = 0
	for opt in options:	
		o = opt[0]
		a = opt[1]
		
		if o == '--include' or o == '-I':
			include_path.append (a)
		elif o == '--version':
			print_version ()
			sys.exit  (0)

		elif o == '--format' or o == '-o':
			__main__.format = a
		elif o == '--outname' or o == '-o':
			if len(files) > 1:
				#HACK
				sys.stderr.write("Mudela-book is confused by --outname on multiple files")
				sys.exit(1)
			outname = a
		elif o == '--outdir' or o == '-d':
			outdir = a
		elif o == '--help' or o == '-h':
			help ()
		elif o == '--dependencies':
			do_deps = 1
		elif o == '--default-mudela-fontsize':
			default_music_fontsize = string.atoi (a)
		elif o == '--init':
			initfile =  a
	
	identify()

	for input_filename in files:
		file_settings = {}
		if outname:
			my_outname = outname
		else:
			my_outname = os.path.basename(os.path.splitext(input_filename)[0])
		my_depname = my_outname + '.dep'		

		(input, deps) = read_tex_file (input_filename)
		(input, muds) = find_mudela_sections (input)
		output = make_files  (input, muds, my_outname)
		output = completize_preamble (output)

		foutn = my_outname + '.' + format
		sys.stderr.write ("Writing `%s'\n" % foutn)
		fout = open (foutn, 'w')

		fout.write (output)
		fout.close ()

		if do_deps:
			write_deps (my_depname, my_outname, deps)

main()
