# This file is part of Rubber and thus covered by the GPL
# (c) Emmanuel Beffara, 2003--2006
"""
This module is used to produce a final self-contained version of the source of
a document, as may be required when preparing a manuscript for an editor. It
mainly replaces \\input and \\include macros by the contents of the files they
include, and replaces bibliography macros by the contents of the bbl file.
The expansion also works in a naive way with local packages and classes,
though the result is likely to fail if a local package uses options.

This package accepts the following options (separated by commas):
  - class : expand \\documentclass even when the class is local
  - nopkg : don't expand \\usepackage even for local packages
  - nobib : don't include the bibliography explicitly
"""

import sys
from os.path import *
import string, re

import rubber
from rubber import _, msg
from rubber.util import *

class Module (Depend, rubber.rules.latex.Module):
	def __init__ (self, doc, dict):

		# register as the post-processor

		if doc.env.final != doc:
			msg.error(_("there is already a post-processor registered"))
			sys.exit(2)

		self.doc = doc
		self.out = doc.src_base + "-final.tex"

		# FIXME: we make foo-final.tex depend on foo.dvi so that all auxiliary
		#   files are built when expanding, this could be optimised...

		Depend.__init__(self, doc.env,
				prods=[self.out], sources={ doc.prods[0]: doc })
		doc.env.final = self

		# initialise the expansion table
		
		self.hooks = {
			"input" : doc.h_input,
			"include" : doc.h_include,
			"usepackage" : self.x_usepackage,
			"RequirePackage" : self.x_usepackage,
			"bibliography" : self.x_remove,
			"bibliographystyle" : self.x_bibliographystyle,
			"end{document}" : doc.h_end_document
		}

		self.pkg_hooks = {
			"NeedsTeXFormat" : self.x_remove_b,
			"ProvidesPackage" : self.x_remove_b,
			"DeclareOption" : self.x_option,
			"ProcessOptions" : self.x_process
		}

		if dict.has_key("opt") and dict["opt"]:
			for opt in string.split(dict["opt"], ","):
				if opt == "class":
					self.hooks["documentclass"] = self.x_documentclass
				elif opt == "nopkg":
					del self.hooks["usepackage"]
					del self.hooks["RequirePackage"]
				elif opt == "nobib":
					del self.hooks["bibliography"]
					del self.hooks["bibliographystyle"]

		self.opt_lists = []   # stack of package option lists
		self.opt_texts = []   # stack of used options

	def run (self):
		msg.progress(_("writing %s") % (self.out))
		self.out_stream = open(self.out, "w")

		# This is sort of a hack: we replace the 'seq' and 'hook' fields in
		# the environment with our own, in order to reuse the parsing routine.

		doc = self.doc
		saved_seq = doc.seq #; doc.seq = self.seq
		saved_hooks = doc.hooks ; doc.hooks = self.hooks
		doc.update_seq()
		try:
			try:
				self.expand_path(self.doc.source())
			except rubber.EndDocument:
				self.out_stream.write("\\end{document}\n")
		finally:
			doc.hooks = saved_hooks
			doc.seq = saved_seq

		self.out_stream.close()
		self.doc.env.something_done = 1

	def expand_path (self, path):
		# self.out_stream.write("%%--- beginning of file %s\n" % path)
		file = open(path)
		try:
			self.doc.do_process(file, path, dump=self.out_stream)
		finally:
			file.close()
		# self.out_stream.write("%%--- end of file %s\n" % path)

	#
	#  The simple expansion hooks
	#

	def x_remove (self, dict):
		pass

	def x_documentclass (self, dict):
		if not dict["arg"]: return
		file = self.env.find_file(dict["arg"] + ".cls")
		if file:
			self.out_stream.write("\\makeatletter\n")
			self.expand_path(file)
			self.out_stream.write("\\makeatother\n")
		else:
			self.out_stream.write(dict["match"])

	def x_bibliographystyle (self, dict):
		if not dict["arg"]: return
		bbl = self.doc.src_base + ".bbl"
		if exists(bbl):
			self.expand_path(bbl)

	#
	#  Package expansion
	#

	def x_usepackage (self, dict):
		if not dict["arg"]: return
		remaining = []

		# Dump the contents of local packages.

		for name in string.split(dict["arg"], ","):
			file = self.env.find_file(name.strip() + ".sty")
			if file and not exists(name + ".py"):

				# switch to package mode if needed

				if self.opt_lists == []:
					self.out_stream.write("\\makeatletter\n")
				if dict["opt"] is None:
					self.opt_lists.append([])
				else:
					self.opt_lists.append(string.split(dict["opt"], ","))
				self.opt_texts.append("")

				# register new macros

				for key, val in self.pkg_hooks.items():
					self.doc.hooks[key] = val
				self.doc.update_seq()

				# expand the package

				self.expand_path(file)

				# switch back to normal mode

				self.opt_lists.pop()
				self.opt_texts.pop()
				if self.opt_lists == []:
					self.out_stream.write("\\makeatother\n")
					for key in self.pkg_hooks.keys():
						del self.doc.hooks[key]
					self.doc.update_seq()

			else:
				remaining.append(name)

		# Rewrite a '\usepackage' for the remaining packages

		if remaining != []:
			self.out_stream.write("\\usepackage")
			if dict.has_key("opt") and dict["opt"]:
					self.out_stream.write("[%s]" % dict["opt"])
			self.out_stream.write("{%s}" % string.join(remaining, ","))

	def x_remove_b (self, dict):
		"""
		This is used to remove a macro and a possibly following argument in
		brackets, as in \\ProvidesPackage{foo}[2003/10/15].
		"""
		print "FIXME: x_remove_b"

	def x_option (self, dict):
		"""
		Parse an option definition.
		"""
		line = string.lstrip(dict["line"])
		if len(line) == 0 or line[0] != "{":
			print "FIXME: option %r"
			return
		arg, next = match_brace(line[1:])
		self.opt_texts[-1] = self.opt_texts[-1] + arg
		dict["line"] = next

	def x_process (self, dict):
		self.out_stream.write(self.opt_texts[-1])
