#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
import os.path
import string
import sys
import time
import re
# May not be needed if not used.
try: import hebgrk
except ImportError: pass


##############################################################################
# CREATE SLIDES FROM SLIDE DEFINITION FILES
##############################################################################
#
# Usage: makeslides
#
# Input files have the names 'N.txt' (where 'N' is a number).
# This tool creates corresponding output files named 'N.htm'
#
# mailto:contact@SpiritAndTruth.org
#
#

# Comment lines begin with the pound sign.
#
#
# Macro definitions begin with double underscore and must be the
# first nonwhitespace in their line.
#
# They follow the form:
#
#     __name|<before>|<endword>|<endline>|<endblock>
#     additional text
#     spanning several lines
#     and ending with a blank (empty) line
#
#         '__name' can be any alphanumeric tag which begins with 
#                  double underscore. There are several special macro names
#                  which are reserved:
#
#                  __banner    designates banner title when invoked
#                              (appears in title bar and splash page)
#
#                  __title     designates slide titles when invoked
#
#                  __audio     designates slide audio file when invoked
#
#                  __include   designates a file to read macros and content
#                              from
#
#                  __greek     convert and display Greek characters according
#                              to BSTGreek font codes.
#
#                  __hebrew    convert and display Hebrew characters according
#                              to BSTHebrew font codes.
#
#         '<before>' is text to be printed prior to the text given 
#                    with the macro when it is invoked
#
#         '<endline>' is text printed after the first word of text
#                    with the macro when it is invoked
#
#         '<endline>' is text printed after the first line of text given 
#                    with the macro when it is invoked
#
#         '<endblock>' is text printed after the any block of text 
#                     defined as part of the macro when it is invoked.
#
#
# Invoke a predefined macro either as:
#
#     __name optional text included
#
# Which, in the above example, will generate:
#     
#     <before>optional<endword>optional text included<endline>
#     additional text
#     spanning several lines
#     and ending with a blank (empty) line
#     <endblock>

#############################################################################

VERSION = 0.6
# 0.6 - Generates associated index.m3u.htm file which proves a human-readable
#       playlist for situations where a browser fails to handle index.m3u
#       properly. User can choose to launch index.m3u from therein if desired.
#
#       If can't find titles and audios from \d+.txt files, then assumes
#       slides.js was manually created and attempts to parse it to get the
#       needed data.
#       
#       If slides.js configuration file already exists, we won't rewrite it
#       unless passed --force flag.
#
# 0.5 - Greek and Hebrew macros automatically generate transliteration.
# 0.4 - Quotes (&quot;) are now separated from the surrounding text using
#       non-breaking spaces so quotes are never hanging alone on the beginning 
#       or end of a line.
# 0.3 - Added support for nested macros in block macros (but still must
#       be first non-whitespace on a line)
# 0.2 - Major revision to keywords, now use generic macro
#       substitutions.
# 0.1 - initial version

##############################################################################
# Creates HTML slides from text files using custom macros.
##############################################################################

SLIDES_FILE_NAME = 'slides.js'
FORCE_FLAG = '--force'
class Converter :
    SEPARATOR = '|'

    def inFileError( self, string ) :
        # report an error in an input file
        print self.inFileName, 'line ', self.lineNumber, string
        assert 0

    def write( self, string ) :
        # write to the output file
        if self.debug:
            print string
        self.outFile.write( string )

    def writeSlideFile( self ) :
        # write to the slide configuration file
        
        # If slide definition \d+.txt files weren't found.
        if not self.titles:
            # A manually-created slide definition file must exist.
            assert os.path.exists(SLIDES_FILE_NAME), \
                "Missing slide file [%s], but found no titles!" % \
                SLIDES_FILE_NAME
            print "WARNING: leaving manually-created slide file unaltered."
            return

        try:

            print 'Creating slide configuration file: ' + SLIDES_FILE_NAME
            slideFile = open( SLIDES_FILE_NAME, 'w' )

            slideFile.write( ''' 
// Automatically generated by makeslides.exe program.
// HAND EDITED CHANGES WILL BE LOST whenever makeslides.exe
// is run again!
//
// Author: Tony Garland
// Info: contact@SpiritAndTruth.org
//
''' )
            slideFile.write( ''' 

// Logo, banner, copyright, home, and info link on splash screen.
//
''')
            slideFile.write( 'window.logo = \'<a href="http://www.SpiritAndTruth.org"><IMG src=spiritandtruth.gif border=0 alt="SpiritAndTruth.org"></a>\'\n')
            slideFile.write( 'window.banner = "' + self.banner + '"\n' )
            year = time.strftime( "%Y", time.localtime( time.time() )  ) 
            slideFile.write( 'window.copyright = \'&copy; %s <a href=\"http://www.SpiritAndTruth.org\">SpiritAndTruth.org</a>\'\n' % year )
            slideFile.write( 'window.home = \'http://www.SpiritAndTruth.org\'\n')
            slideFile.write( 'window.info = \'<a href="mailto:contact@SpiritAndTruth.org">SpiritAndTruth.org</a>\'\n')

            slideFile.write( ''' 

// List of titles for the slides.
//
''')
            slideFile.write( 'window.titles = [\n' )
            self.writeQuotedList( slideFile, self.titles )
            slideFile.write( ']\n' )

            slideFile.write( ''' 

// List of slide files.
//
''' )
            slideFile.write( 'window.slides = [\n' )
            self.writeQuotedList( slideFile, self.slides )
            slideFile.write( ']\n' )

            slideFile.write( ''' 
            
// List of audio files.
//
''' )
            slideFile.write( 'window.audios = [\n' )
            self.writeQuotedList( slideFile, self.audios )
            slideFile.write( ']\n' )

            slideFile.write( '''

// File to launch to stop audio playback.
// Change if your player does not support MP3.
// Make a short empty recording and use that
// instead.
''')
            slideFile.write( 'window.silence = "silent.mp3"' )

            slideFile.write( '''

// Auto advance slides every N seconds?
// Set to zero to disable.
''')
            slideFile.write( 'window.advance = 0' )

            slideFile.write( '''

// Automatically start slide show at specified slide?
// Set to zero to disable. (Slide show will start
// at splash screen if autostart is set to zero.
// User must press 'start' button to start the show.
'''
)
            slideFile.write( 'window.autostart = 0' )

        finally:
            slideFile.close()

    def parseSlideFile( self ) :
        # Parse the slide definition file for titles and audio files.

        # If we've already found titles (and audios) from \d+.txt files,
        # then we generated the slide definition file ourselves so no need to
        # parse it.
        if self.titles : return

        slideFile = open( SLIDES_FILE_NAME, 'r' )
        slideLines = slideFile.readlines();
        inTitles = False
        inAudios = False
        self.banner = None
        for line in slideLines :
            line = line.strip()
            if ( re.search('window.banner', line ) ) :
                self.banner = re.sub( '[^"]*"(.*)".*', r'\1', line )
                continue
            if ( re.search('window.titles', line ) ) :
                inTitles = True
                continue
            if ( re.search('window.audios', line ) ) :
                inAudios = True
                continue
            if inTitles :
                if ( re.search('^\s*]', line) ) :
                    inTitles = False;
                    continue;
                title = re.sub( '[^"]*"(.*)".*', r'\1', line )
                assert title
                #assert 0, "[%s]" % title
                self.titles.append( title )
            if inAudios :
                if ( re.search('^\s*]', line) ) :
                    inAudios = False;
                    continue;
                audio = re.sub( '[^"]*"(.*)".*', r'\1', line )
                assert audio
                #assert 0, "[%s]" % audio
                self.audios.append( audio )

        assert self.banner, "Can't find banner in %s!" % SLIDES_FILE_NAME
        assert self.titles, "Can't find titles in %s!" % SLIDES_FILE_NAME
        assert self.audios, "Can't find audios in %s!" % SLIDES_FILE_NAME

    def writePlayListFile( self ) :
        # write to the play list.

        assert self.titles, 'Unable to parse slide information!'
        if len(self.titles) != len(self.audios) :
            print 'Slide vs. audio count mismatch!'
            print 'titles = ', len(self.titles)
            print 'audios = ', len(self.audios)
            print self.titles
            print self.audios
            assert len(self.titles) == len(self.audios)

        playList = None
        playListHtm = None
        try:

            playListName = 'index.m3u'
            playListNameHtm = 'index.m3u.htm'

            playList = open( playListName, 'w' )
            playListHtm = open( playListNameHtm, 'w' )

            print 'Generating %s and %s' % ( playListName, playListNameHtm )

            playList.write( '#EXTM3U\n' );
            playListHtm.write('''
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML>
<HEAD>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8;">
<TITLE>%s</TITLE>
</HEAD>
<BODY>
<H1 align="center">%s</H1>
<P align="center">
<A HREF="index.m3u">CLICK HERE</a> to listen to the playlist.
</P>
<P></P>
<P>
Some web browsers do a poor job of handling audio playlists in
which case you can click through the list below instead.
</P>
<H2>
<OL>
''' % (self.banner, self.banner) )


            duration=None
            mp3=None
            try:
                import mutagen
                from mutagen import mp3
            except ImportError :
                print "Can't find mutagen, using streaming audio duration of '-1'"
                seconds = "-1"
                pass

            for i in range( len( self.audios ) ) :
                title = self.titles[ i ]
                audio_path = self.audios[ i ]

                if mp3 : 
                    audio_info = mp3.MP3( audio_path )
                    seconds = int(audio_info.info.length)
                    minutes = int( seconds / 60 )
                    remainder = seconds - ( minutes * 60 )

                playList.write( 
                    '#EXTINF:%s,Slide %d - ' % (seconds, ( i + 1 ) ))
                playList.write( title + '\n' )
                playList.write( audio_path + '\n' )

                playListHtm.write(
                    '<LI><A HREF="%s">%s</A> (%02d:%02d)</LI>\n' % (
                        audio_path, title, minutes, remainder ) )

            playListHtm.write('</OL>\n</H2>\n')

            playList.write('''

###########################################################
#  www.SpiritAndTruth.org Webshow Play List
#         contact@SpiritAndTruth.org
###########################################################
#
# This file contains a list of audio files to be played
# in sequence. 
#
# iF YOUR MEDIA PLAYER WILL NOT PLAY THIS FILE
# PROPERLY, YOU MAY NEED TO UPGRADE TO A NEWER VERSION
# OR DIFFERENT BRAND OF MEDIA PLAYER.
#
# Older media players cannot play this file when it resides
# on a website--only when it is played from a local hard
# drive or CDROM. Newer media players can play it directly
# from the website.
# 
# If you would like to obtain this play list with
# all the associated audio files, look for an
# associated webshow download file with a .Z, .ZIP, or 
# .TGZ extension on our website.
#
''')

            playListHtm.write('''
<P>
<B>NOTE:</B>
The M3U playlist file refers to the location of each audio segment using 
a relative path. If the M3U file is moved from its original location, 
then each of the audio segment recordings must also be copied with it 
or the M3U file won't be able to find them during playback.
</P>
</BODY>
</HTML>
''');

        finally:
            if playList : playList.close()
            if playListHtm : playListHtm.close()

    def writeQuotedList( self, file, list ) :
        # write a list as quotes strings, one per line
        for name in list :
            if name is list[ -1 ] :
                file.write( '"%s"\n' % name )
            else :
                file.write( '"%s",\n' % name )

    def cmpFiles( self, x, y ) :
        # Sort files beginning with digits using numeric comparisons
        # (Crude and not strictly correct, but gets the job done
        # for our purposes.)
        matchX = re.match( '\d+', x )
        matchY = re.match( '\d+', y )
        if matchX and matchY :
            x = int( matchX.group() )
            y = int( matchY.group() )
            #print x, y
        return cmp( x, y )


    def __init__( self, args ) :
        # initialize and run the converter
        self.force = False
        self.lineNumber = 0
        self.inFile = None
        self.outFile = None
        self.inFileName = ''
        self.debug = 0
        self.banner = 'Slide Presentation'
        self.macros = {}
        self.macroTexts = {}
        self.textBlock = []
        self.slides = []
        self.audios = []
        self.titles = []
        self.iFile = -1
        self.banner = ''
        self.lastMacro = ''
        self.openMacro = ''
        self.m_outputBuffer = ''

        if FORCE_FLAG in args :
            self.force = True
            args.remove( FORCE_FLAG )

        #macroPat = '(?P<macro>^__\w+)(?P<sep>\S)?(\s+)?(?P<remainder>(?P<firstword>\S+)?(\s+)?(?P<otherwords>.*))'
        #macroPat = '^\s*(?P<macro>__\w+)(?P<sep>\S)?(\s+)?(?P<remainder>(?P<firstword>\S+)?(?P<otherwords>\s*.*))'
        macroPat = '^\s*(?P<macro>__\w+)(?P<sep>\S)?(\s+)?(?P<remainder>(?P<firstword>\S+)?\s*(?P<otherwords>.*))'
        self.macroExp = re.compile( macroPat )

        # Set up predefined macros.
        self.defineMacro( '__banner', Converter.SEPARATOR, self.banner )
        self.defineMacro( '__title', Converter.SEPARATOR, 'Title' )
        self.defineMacro( '__include', Converter.SEPARATOR, '' )

        # For all files in the current directory.
        if self.force or not os.path.exists( SLIDES_FILE_NAME) :

            files = os.listdir( '.' )
            files.sort( self.cmpFiles )
            for self.inFileName in files :

                # Skip all files except slide definitions.
                fileLower = string.lower( self.inFileName )
                match = re.match( '\d+.txt', fileLower )
                if not match : continue

                # Decode the macros and content in the file.
                self.decodeFile( self.inFileName )

            # Create the slide configuration file.
            self.writeSlideFile()

        else :
            print 'Use', FORCE_FLAG,  'to regenerate slide files and', SLIDES_FILE_NAME

        # Parse slide configuration file (may have been manually created
        # and we didn't generate it.
        self.parseSlideFile()

        # Create the audio play list file.
        self.writePlayListFile()

    def decodeFile( self, inFileName, isIncludeFile = 0 ) :
        self.iFile += 1
        inFile = None
        outFile = None
        try :

            # May be a nested call handling an include file.
            inFileSave = self.inFile
            outFileSave = self.outFile
            inFileNameSave = self.inFileName
            self.inFileName = inFileName
            lineNumberSave = self.lineNumber

            # Open input file.
            inFile = open( inFileName, 'r' )
            parts = string.split( inFileName, '.' )

            # Open output file.
            outFileName = re.sub( '.txt', '.htm', inFileName )
            self.outFile = open( outFileName, 'w' )
            #self.write( '<html>' )
            if not isIncludeFile : self.slides.append( outFileName )

            print inFileName, '-->', outFileName

            # Append an empty line to the user's definition so
            # it will cause any previously pending macro to be closed and
            # flushed.
            lines = inFile.readlines()
            lines.append( '\n' )

            # Accumulate output in a buffer for optimizations.
            self.m_outputBuffer = ''

            # For each line.
            self.lineNumber = 0
            for line in lines :
                self.lineNumber = self.lineNumber + 1
                #print self.lineNumber

                self.decodeLine( line )

            # Flush the full file output.
            if self.m_outputBuffer : 
                self.flushOutput( self.m_outputBuffer )

            # Restore previous file info (may be recursive call
            # for include file).
            self.inFileName = inFileNameSave 
            self.inFile = inFileSave 
            self.outFile = outFileSave 
            self.lineNumber = lineNumberSave 

        # Close files.
        finally:

            if inFile : inFile.close()
            if outFile : outFile.close()

    # Is a multi-line macro currently open?
    def macroOpen( self ) :
        open = 0
        if self.macros.has_key( self.openMacro ) :
            macroParts = self.macros[ self.openMacro ]
            # Explicitly marked open or implicitly has enough parts.
            open = ( 4 <= len( macroParts ) )
        return open

    # Close open macro.
    def closeMacro( self ) :
        #print 'FOO CLOSEMACRO', self.openMacro
        # If there was a previous macro open.
        if self.macroOpen() :

            # It owns any previous text block as part of it's definition.
            if self.textBlock :
                #print 'FOO SAVE MACROTEXT', self.openMacro, self.textBlock
                self.macroTexts[ self.openMacro ] = self.textBlock
                self.textBlock = []

            # The macro is now closed.
            self.openMacro = ''

    # Save items which may be duplicated in each file (only retain last).
    def savePerFile( self, list, text ) :
        if len( list ) <= self.iFile :
            list.append( text )
        else :
            list[ self.iFile ]= text

    # Decode lines and handle macros
    def decodeLine( self, line ) :

        # Skip comments
        #if '/' == line[ 0 ] : return
        if re.match( '^\s*/', line ) : return

        # Removed carriage return and line feed.
        if line and '\n' == line[ -1 ] : line = line[ 0 : -1 ]
        if line and '\r' == line[ -1 ] : line = line[ 0 : -1 ]


        #print '*************************', '\n', self.lineNumber, line 

        # Skip empty lines.
        if not line or not re.search( '\S', line ) :
            self.closeMacro()
            return

        # If looks like a keyword.
        match = self.macroExp.match( line )
        if match :
            macro = match.group( 'macro' )
            sep = match.group( 'sep' )
            remainder = match.group( 'remainder' )
            firstword = match.group( 'firstword' )
            otherwords = match.group( 'otherwords' )

            #print self.inFileName, ':', self.lineNumber, ':', line

            # If defining or redefining a macro
            if sep :

                # Save new macro information.
                self.defineMacro( macro, sep, line )

                # Predefined macros may be redefined.
                if '__title' == macro :
                    self.savePerFile( self.titles, remainder )
                if '__banner' == macro :
                    self.banner = remainder

            # Else no separator so
            # invoking a previously-defined macro.
            else :

                # If the previous macro is still open.
                if self.macroOpen() :

                    # If the macro that is open is now being invoked.
                    # (We don't allow recursive macros.)
                    if self.openMacro == macro :

                        # Close the macro.
                        self.closeMacro()

                    # Else this is a different macro than the open one.
                    else :

                        # Add this nested macro to the text block.
                        #print 'FOO TEXTAPPEND1', line
                        self.textBlock.append( line )

                # Else the previous macro is closed.
                else :

                    output = ''

                    # Output any text block.
                    block = ''
                    for textLine in  self.textBlock :
                        output += ( textLine + '\n' )
                    self.textBlock = []

                    # Title and banner may be given macro invocation.
                    if remainder :
                        if '__title' == macro :
                            self.savePerFile( self.titles, remainder )
                        if '__banner' == macro :
                            self.banner = remainder


                    # If specifying an audio file.
                    if '__audio' == macro :

                        if len( self.audios ) <= self.iFile :
                            self.savePerFile( self.audios, remainder )
                        #print 'audio', remainder
                        #assert 0

                    # Else if specifying an included file.
                    elif '__include' == macro :

                        self.flushOutput( output )
                        output = ''

                        self.decodeFile( remainder, 1 )

                    # Else if custom transliteration macro.
                    elif '__translit' == macro :
                        text = firstword + ' ' + otherwords
                        assert( hebgrk )
                        text = hebgrk.translit( text )
                        output += '<span class="translit">'
                        output += text 
                        output += '</span>'

                    # Else if custom Greek macro.
                    elif '__greek' == macro :
                        text = firstword 
                        if otherwords: text += ' ' + otherwords
                        assert( hebgrk )
                        greek = hebgrk.BSTGreekToUnicodeHtml( text )
                        output += ( '<span class=ugreek>%s</span>' % greek )
                        output += ' [<span class="translit">'
                        output += hebgrk.translitToUnicodeHtml(
                            hebgrk.translitBSTGreek( text ))
                        output += '</span>]'

                    # Else if custom Hebrew macro.
                    elif '__hebrew' == macro :
                        text = firstword
                        if otherwords: text += ' ' + otherwords
                        assert( hebgrk )
                        # Don't reverse since the unicode font we use
                        # automatically displays in reverse order.
                        hebrew = hebgrk.BSTHebrewToUnicodeHtml( text, 0, 0 )
                        output += ( '<span class=uhebrew>%s</span>' % hebrew )
                        output += ' [<span class="translit">'
                        output += hebgrk.translitToUnicodeHtml(
                            hebgrk.translitBSTHebrew( text ))
                        output += '</span>]'

                    # Not an audio file.
                    else :

                        # If we haven't seen it yet.
                        if not self.macros.has_key( macro ) :

                            # Create a default entry for it.
                            #self.defineMacro( macro, '', '' )

                            self.inFileError( 'Macro must be defined before it is used: ' + macro)

                        # Print the leading part.
                        parts = self.macros[ macro ]
                        if 1 <= len( parts ) : 
                            output += parts[ 0 ]

                        # Print the first word.
                        if firstword :
                            output += firstword

                        # Print after first word.
                        if 2 <= len( parts ) and parts[ 1 ]: 
                            output += parts[ 1 ]
                        elif otherwords : 
                            output += ' ' 

                        # Print the rest of the words on the macro
                        # line.
                        if otherwords :
                            output += otherwords

                        # Print the end of line.
                        if 3 <= len( parts ) : 
                            output += parts[ 2 ]

                        # Print any macro text block.
                        if self.macroTexts.has_key( macro ) :
                            #print 'FOO PRINT MACROTEXT', self.macroTexts[ macro ]
                            if 0 :
                                output += ( '\n' + self.macroTexts[ macro ] )
                            else :
                                for mline in self.macroTexts[ macro ] :
                                    #print 'FOO MLINE', mline

                                    self.flushOutput( output )
                                    output = ''

                                    # Recursive call to scan nested macros.
                                    saveBlock = self.textBlock
                                    saveOpen = self.openMacro
                                    saveLast = self.lastMacro
                                    self.textBlock = []
                                    self.openMacro = ''
                                    self.lastMacro = ''

                                    self.decodeLine( mline )

                                    block = self.textBlock
                                    self.textBlock = saveBlock
                                    self.openMacro = saveOpen
                                    self.lastMacro = saveLast

                                    for bline in block : 
                                        output += ( bline + '\n' )

                        # Print end of block.
                        if 4 <= len( parts ) :
                            output += parts[ 3 ]

                    self.flushOutput( output + '\n' )
                    output = ''

            # Remember the previous macro we've seen.
            self.lastMacro = macro

        # Else not a keyword.
        else :

            # Save text for subsequent output.
            self.textBlock.append( line )


    def flushOutput( self, text ) :
        # If finally flushing at end of processing file.
        if text is self.m_outputBuffer :

            # Perform spacing optimizations.

            # Remove extra spaces.
            # Goofs up helpful nesting in javascript so don't do this.
            #text = re.sub( ' +', r' ', text )

            # Remove extra spaces within braces.
            text = re.sub( '([\(\[])(\s+)', r'\1', text )
            text = re.sub( '(\s+)([\)\],;.])', r'\2', text )

            # Quotes must not break from adjacent text.
            text = re.sub( '((&nbsp;|\s)+)&quot;', '&nbsp;&quot;', text )
            text = re.sub( '&quot;((&nbsp;|\s)+)', '&quot;&nbsp;', text )

            # Remove spaces around end-specific quotes.
            text = re.sub( '(&#14(5|7);)((&nbsp;|\s)+)', r'\1', text )
            text = re.sub( '((&nbsp;|\s)+)(&#14(6|8);)', r'\3', text )

            # Remove extra spaces prior to punctuation
            text = re.sub( ' +((<[^>]+>)?[\.?;,:])', r'\1', text )

            self.write( text )

            # Prepare for new output.
            self.m_outputBuffer = ''

        # Else flushing when part way done.
        else :

            # Accumulate output ready for final optimizations.
            self.m_outputBuffer += text

    def defineMacro( self, macro, sep, line ) :
        parts = line.split( sep )
        self.macros[ macro ] = parts[ 1 : ]
        #print 'FOO MACRO PARTS', macro, self.macros[ macro ]
        self.openMacro = macro





# If module is launched as a program, perform indexing using specified files.
if __name__ == '__main__' :
    convert = Converter( sys.argv )
