#!/usr/bin/python

# Parallel2 - 
# An interactive lerning tool of the flip-card kind.
# see http://www.lojban.org/tiki/tiki-index.php?page=Parallel+2 for
# further information.


#------------------------------------------------------------
# Config: 
#------------------------------------------------------------
# You can set the location of your data files below.
# The DATAPATH is usually where you unpacked the
# Datafiles that came with the program. There 
# you will find your Lojban_***.txt files, it should
# also most likely have several directories containing
# all the sound files. The default, "./" means
# the program looks in the same directory this script is in.
#------------------------------------------------------------

DATAPATH = "./"

#------------------------------------------------------------
# set the sound player used to play sounds files.)
#------------------------------------------------------------

PLAYERCOMMAND = "mplayer -ao alsa"

#------------------------------------------------------------
# set a larger window size (Horisontal,Vertical) in pixels.
# (the default 800x400 should be considered a minimum.)
#------------------------------------------------------------

WINSIZE = (800, 400)





############# Program source code #####################
#      (nothing to modify below unless you know       #
#                what you are doing ...)              #
#######################################################

#-----------legal blurb and coder comments---------
#
# Hopefully this is free software, you should be able to use, modify,
# and redistribute non-commercially freely, as long as you grant
# these same rights to anyone who wishes to have them... or so... :-)

# 2008-10-22, griatch (griatch; www.griatch-art.deviantart.com)
# Version 0.4
# Restructured entire code to make it fully object oriented and added
#  lots of docstrings to help future modifications. 
# Added 6 display modes, from normal/random to fully automatic with auto-voice
# User input possible at all times (also during automatic modes)
# On-screen countdown for automatic modes (can be turned on/off)
# Ability to pause, speed/slow, and skip countdown
# Hearing comprehension: Hide lojban text and auto-play sound in all modes
# New Welcome screen and Help screen (space key)
# Less procssor usage

# 2008-05-30, ydo <tobbe at tobbe dot nu>
# modified to use oggs or mp3s if they exist, the wave format in data.tar.gz
# was obscure and not readable by mplayer (libmad)

# 2007-11-14, twey (twey at twey dot co dot uk)
# added a line to quit PyGame's mixer, freeing up the audio device
# for mplayer/realplayer/player of choice.

# 30/01/07, thkruege at googlemail dot com
# made radical changes and therefore introduced next version (0.3)
# Is it ok to regard this as GPLed software?
# (for changes, see below)

# 20/01/06 curv (Kio) Modified to fit on smaller screens,
# replaced string.rjust() with string.zfill(), changed default
# colours to be easier on the eyes. (Matter of opinion of course)

# December 2005, Yann Le Du http://yledu.free.fr
# Second improved version

# Directions for use :
# Prerequisites : python, pygame with sdl and ttf, your favourite
# wav-player (default: mplayer), data.tar.gz (Parallel 2 data files from
# http://www.lojban.org/tiki/tiki-download_wiki_attachment.php?attId=225&download=y )
# run this program from the directory where the parallel 2 data files
# reside. Or specify a path to the data directory below
# Run it with python by typing "python parallel03.py"

# Changes by thkruege at googlemail dot com:

# - introduced proper versioning
# - keyboard commands now pygame based, therefore:
# - does not need a terminal for inputs any more
# - does not depend on sdl-mixer any more (for now)
# - plays audio files using custom command (or not at all)
#     (this got rid of the rather nasty realplayer dependency)
# - data can now reside in arbitrary locations
# - slightly changed "gameplay"
#     (now you only listen to sounds by pressing an extra key)
# - threw away some redundant code
# - threw away unused variables
# - reorganized code so that global variables are easyier to access
#     for non developers
# - fixed SDL-ttf-render fail with some voodoo
# - fixed a typo ;-)

# TODO:
# - allow keyboard interrupts in scrolling/random mode
# - get rid of all those ugly hacks
# - get rid of all this hardcoded stuff
# - implement audio output via pygame (sdl-mixer)
# - clean up code, do object orientation
# - implement more colours
# - document properly
# - ask author if its ok to license under GNU GPL
# - (maybe repackage data.tar.gz with compressed audio files)

# CREDITS:
# Original idea and Windows version "Parallel 2" by Yanis Batura
# Some pygame technique taken from "Learnfast" program by Michal Wallace

#---------------------end credits---------

# import modules

import os, sys, string, time
import random, math, pygame
from pygame.locals import *
import pygame.font
import pygame.key
import random
import re

# useful constants

YELLOW = (255,255,0)
BLACK = (0,0,0)
CYAN = (0,255,255)
RED  = (255,0,0)
GREY = (100,100,100)

CENTER = (WINSIZE[0]/2,WINSIZE[1]/2)
TOP = CENTER[1] - 60
LOWER = int(WINSIZE[1]/1.5)
FOOTER = WINSIZE[1]

# -------- helper classes---------

class Data(object):
    """Objects of this class stores one English/Lojban example by
    reading it from Data files upon creation of object instances.
    The names of those files are tied to the 'level' the program operates in,
    and thus the paths and names are hard coded for each level (this
    is easy to change).

    The Data class supplies several useful member functions
    intended for convenient access to aspects of the data,
    such as getting a particular English/Lojban sentence for
    display.
    """
    def __init__(self,level='0'):

        self.level = level
        self.data  = [] 
        
        if level == '0':
            datafile = "Lojban Pronunciation.txt"
            datadir = "Pronunciation"
        else:
            datafile="Lojban" + level.zfill(3) + "_Eng.txt"
            datadir = "Lojban_" + level.zfill(3)

        filepath = DATAPATH + "/" + datafile
        try:
            f = open(filepath,"r")
            total = f.readlines()
            f.close()
        except IOError:
            raise IOError,filepath

        counter=0
        symplus = False
        toplay = ""
        working = total[1:]+["  "]
        timefortwo = 0.4

        #The time for the break between words in the sound files.
        #This is longer for the initial letter-pronounciation example.
        if level == 0:
            timeforspace = 1.1
        else:
            timeforspace = 0.3

        # The data contains the English sentence and Lojban translation.
        # the self.data list contains separate "sets" that describe each
        # question(s) + answer(s) pairing 
        # [ [1st English, 2nd English, etc.],
        #   [1st Lojban, 2nd Lojban, etc.],
        #   [wavfilename, time of play] ].
        # An example may have multiple sentences, i.e. question+answer

        set=[[],[],[]]

        for w in working:

            if w[1]=="+":
                word = string.join(w[2:].split("\x00"),"").strip()

                # A Lojban answer to a question
                if symplus: 
                    symplus = False
                    set[1].append(word)
                    much = sum([len(m)/2.*timefortwo for m in word.split(" ")])\
                           +word.count(" ") * timeforspace # Time for pronunciation
                    set[2][1] += much # Add time for answer

                # An English answer to a question
                else: 
                    symplus = True
                    set[0].append(word)

            # In this case, we have the Lojban sentence
            elif w[1]=="`":
                # Voodoo for extracting Wav file number from unicode string
                counter = string.join(w.split("`")[1].split("\x00"),"").strip() 
                toplay = datadir + os.sep+counter+".wav" # Wav file name
                # Voodoo for extracting sentence from unicode string
                word = string.join(w.split("`")[2].split("\x00"),"").strip() 
                much = sum([len(m)/2.*timefortwo for m in word.split(" ")]) + \
                       word.count(" ")*timeforspace # Time for pronounciation

                set[1].append(word)
                set[2].append(toplay)
                set[2].append(much)

            # In this case, we have a (new) English sentence
            # Hence we store the previous one in the object.
            else: 
                self.data.append(set) 
                set=[[],[],[]] # and a new example begins
                word = string.join(w.split("\x00"),"").strip()
                set[0].append(word)

            #set some info

            self.datasize = len(self.data)

    #Data Class member functions
    def getText1(self,setnr):
        """This is the text of the 'Question': E.g. an English/lojban text to be translated."""
        return self.data[setnr][0] #this is a list with at least 1 element

    def getText2(self,setnr):
        """This is the 'Answer' text, e.g. the English/lojban translation."""
        return self.data[setnr][1] #this is a list with at least 1 element
    
    def getSoundFileName(self,setnr):
        """This is the sound's filename (a string)"""
        return self.data[setnr][2][0]
    
    def getPauseTime(self,setnr):
        """This time measures how long it takes to say the respective
        sentence. It is used by the auto-changing display modes as
        a measure for how long to wait."""
        return self.data[setnr][2][1]

class SoundPlayer(object):
    """This class handles the playing of sounds using an
    externally defined media player. Obs: This part
    is currently highly *nix specific."""
    
    def __init__(self):
        self.p = re.compile('(/.*?)\.wav')
    def play(self,pos,dataset):
        filename = dataset.getSoundFileName(pos)
        #try to find the full path to this file
        ogg = self.p.sub(r'/cleaned/\1.ogg',filename)
        mp3 = self.p.sub(r'/cleaned/\1.mp3',filename)
        if os.path.exists(ogg):
            sound = ogg
        elif os.path.exists(mp3):
            sound = mp3
        elif os.path.exists(filename):
            sound = filename
        else:
            raise "NoSoundFile",mp3
        #print "Playing '%s'..." % sound
        cmd = PLAYERCOMMAND+" "+sound +" > /dev/null 2>/dev/null &"
        os.system(cmd) # Play the wav
          

class Interface(object):
    """The graphical interface. This class handles everything
    being drawn to the screen, including menus, timers and text.
    Since almost all operations in the program is related to changing
    how things are displayed, it also stores several state variables
    that is queried directly when something is to be shown (such as
    the timer or if the listening comprehension is active or not). This
    removes that logic from the main loop.    
    """
    def __init__(self):
        #init the program window
        pygame.init()
        pygame.mixer.quit()
        screen = pygame.display.set_mode(WINSIZE)
        pygame.display.set_caption("Parallel")
        screen.fill(BLACK)
        self.screen = screen
        #setup useful fonts
        self.fontHuge  = pygame.font.Font(None, 33)
        self.fontLarge = pygame.font.Font(None, 30)
        self.fontSmall = pygame.font.Font(None, 23)
        #timer/clock display
        self.speedfactor = 1.0
        self.showclock = False
        surf = self.fontLarge.size("8888")
        self.clockrect = pygame.Rect((CENTER[0]-surf[0]/2,TOP-surf[1]),surf)
        #show answer in text or not (if False->listening comprehension mode)
        self.showlojban = True
        
    def display(self,text,height,color=YELLOW,fontsize="SMALL",linespacing=0.5):
        """This is the program's main display function. It draws text centered
        on screen. It can accept a list of texts; the
        text will then be put in consecutive lines (Pylab's coordinate system
        starts in the upper left corner of the screen, so be aware that if you
        give a height equal to the screen height, you will end up at its bottom)
        """
        if type(text) != type([]):            
            text = [text]
        text = reversed(list(text))         

        hpos = 0.0
        if fontsize == "SMALL":
            font = self.fontSmall
        elif fontsize == "HUGE":
            font = self.fontHuge
        else:
            font = self.fontLarge
        for line in text:            
            w,h = font.size(line)
            w0 = CENTER[0]-(w/2)
            h0 = height - (h + hpos)
            self.screen.blit(font.render(line,1,color),(w0 ,h0))
            hpos += (h * (1.0 + linespacing)) 

        pygame.display.flip()
                        
    def clearScreen(self):
        self.screen.fill(BLACK)

    def errorMessage(self,text):
        """This displays a screen with red text, and waits for the
        user to press any key before going back to where she
        came from."""
        self.clearScreen()
        self.display(text,CENTER[1],color=RED)
        s = "Press any key to continue."
        self.display(s,FOOTER)
        self.waitForKey()

    def display_helpMenu(self):
        s = ["Commands (some only apply to certain display modes):","",
             "(esc) key : Quits the program.",
             "(e)xit : Return to main menu.",
             "(space) key : Pause and show this help.",
             "(h)ear : Hear the sentence spoken out loud.",              
             "(l)isten-mode : Toggles hiding of the 'answer'. It will always play instead.",
             "(p)rev / (n)ext : Step sentences forward/backwards.",             
             "(c)lock : Show timer countdown. This will also restart timer.",
             "(f)aster / (s)lower : Change timer countdown speed.",
             "e(n)d : Go on without waiting for countdown to finish."]
        self.clearScreen()
        self.display(s,LOWER+20,color=GREY)
        self.waitForKey()

    def display_opening(self):
        "A bried entry screen. It disappears after a few seconds."
        self.clearScreen()
        s = "Welcome to Parallel"
        pygame.display.set_caption(s)
        s = ["Welcome to Parallel!","- a program helping you to learn Lojban."]
        self.display(s,CENTER[1],fontsize='HUGE')
        self.waitForKey([],timelimit = 3.0)
        
    def display_dataMenu(self):
        "Shows the menu for picking a 'level'"
        self.clearScreen()
        s = "Parallel Main menu"
        pygame.display.set_caption(s)            
        s = ["Main menu","","Select your level of difficulty:","",
             "Choose 0 for pronounciation basics",
             "or pick a difficulty level:",
             "(easy) 1 -  2 - 3 - 4  (hard)"]
        self.display(s,CENTER[1])
        s = "Select 0 - 4 or (esc) to quit."
        self.display(s,FOOTER)

    def display_modeMenu(self):
        "Shows the menu for picking a display mode."
        self.clearScreen()
        s = ["Select display mode:","",
             "1) NORMAL : Step through level, press keys to see and/or hear answer.",        
             "2) RANDOM : Like Normal, but sentences appear in random order.","",             
             "3) QUICK : Like Random, but the answer appears after a time delay.",
             "4) QUICKtalk : Like Quick, except the answer is always spoken.","",
             "5) AUTO : Like Quick, but also sentences switch automatically.", 
             "6) AUTOtalk: Like Auto, except the answer is always spoken."]
        self.display(s,LOWER)
        s = ["Select 1 - 6. (e)xit to pick another level, (esc) to quit."]
        self.display(s,FOOTER)
        
    def display_sentenceMenu(self):
        "Menu used for interactive modes."
        s = "(p)rev, (n)ext, (h)ear. (l)isten-mode, (space) or (e)xit."
        self.display(s,FOOTER)

    def display_autoMenu(self):
        "Menu used for automatic modes when timer is running."
        s = "(c)lock, (f)aster, (s)lower), e(n)d, (h)ear, (l)isten-mode, (space) or (e)xit."
        self.display(s,FOOTER)

    def display_askSentence(self,numex,setnr,dataset):        
        "Display the Question, by quering the relevant data package."
        self.clearScreen()
        s = "Parallel - Level %s Example number %s/%s" % \
            (str(dataset.level),str(numex),str(dataset.datasize-1))
        pygame.display.set_caption(s)    
        sentence = dataset.getText1(setnr)        
        self.display(sentence,CENTER[1],fontsize='LARGE')

    def display_answerSentence(self,setnr,dataset):
        "Display the Answer, by quering the relevant data package."
        if self.showlojban:
            sentence = dataset.getText2(setnr)
        else:
            sentence = ["(Listening comprehension mode)"]
        self.display(sentence,LOWER,color=CYAN,fontsize='LARGE')
    
    def display_time(self,timedisplay):
        """In automatic modes, a timer can be made visible, counting
        down. This methods displays the current time in a nice way."""        

        #we must blit over the old clock position to avoid flicker
        self.screen.fill(BLACK,self.clockrect)

        #now display the updated clock value
        self.display(timedisplay,TOP,color=RED,fontsize='LARGE')        
        self.timedisplayold = timedisplay

    def waitForKey(self,acceptedKeys=[],timelimit=None):
        """This function waits patiently for the
        user to give input. If list 'acceptedKeys' is given, it only
        accepts keys in this list. If list is empty, it accepts any key press.
        The escape key always exits the program.

        If the 'timelimit' argument is given (in seconds), the system will automatically
        exit once that time has passed. It will then return None. Note that the timer
        is reset every time the function is entered, so giving consecutive accepted
        commands is a way to 'extend' the pause used by auto modes, for example.

        The function try to conserve cpu load by selecting the most suitable event
        handler for the situation.
        """

        if timelimit:
            clock = pygame.time.Clock()
            maxms = timelimit * 1.E+3 * self.speedfactor #converting seconds to milliseconds    
            t = 0

        while True:            

            if timelimit:
                t += clock.tick()
                if t >= maxms: return None

                if self.showclock and t % 1000 == 0: 
                    self.display_time(str(int(maxms-t)/1000))

                #we have to use this method to be able to keep updating the clock
                eventlist = pygame.event.get()
            else:
                #otherwise we use the wait() method -- it idles here
                #(little cpu use) until user does something
                eventlist = [pygame.event.wait()] 

            for event in eventlist:
                #closing the window works too
                if event.type == QUIT:
                    sys.exit()
                #use key release as activator
                if event.type == KEYUP:

                    if event.key == K_ESCAPE:
                        sys.exit()        
                    if not acceptedKeys:
                        return 
                    if event.key in acceptedKeys:                    
                        if event.key in [K_SPACE]: key = "?" #help                   
                        elif event.key in [K_c]: key = "c" #clock                   
                        elif event.key in [K_e]: key = "e" #end
                        elif event.key in [K_f]: key = "f" #faster
                        elif event.key in [K_h]: key = "h" #hear
                        elif event.key in [K_l]: key = "l" #lojban                        
                        elif event.key in [K_n]: key = "n" #next               
                        elif event.key in [K_p]: key = "p" #previous                   
                        elif event.key in [K_s]: key = "s" #slower
                                                
                        elif event.key in [K_0]: key = "0"
                        elif event.key in [K_1]: key = "1"
                        elif event.key in [K_2]: key = "2"
                        elif event.key in [K_3]: key = "3"
                        elif event.key in [K_4]: key = "4"
                        elif event.key in [K_5]: key = "5"
                        elif event.key in [K_6]: key = "6"                        
                        else:
                            raise "BUG: Wrong acceptedKey in call to waitForKey!"
                        return key
                

#----------------main function-----------------
                            
def main():

    #create objects
    prog = Interface()    
    sound = SoundPlayer()

    #show the opening screen (this returns
    #after key press or after a few seconds)
    prog.display_opening()

    #main program loop
    while True:

        #main menu: select (new) level         
        while True:
            prog.display_dataMenu()
            level = prog.waitForKey([K_0,K_1,K_2,K_3,K_4])

            #try to load the data set for that level
            try:
                dataset = Data(level)
            except IOError,e:                
                s = ["Could not find the data file for level "+str(level)+",",
                     str(e),"Make sure the path is correct.",]
                prog.errorMessage(s)                
                continue
            break        
        
        #display-mode menu (level 0 will ignore this) 
        if level == '0':
            mode = '1'
        else: 
            prog.display_modeMenu()
            mode = prog.waitForKey([K_1,K_2,K_3,K_4,K_5,K_6,K_e])
            if mode == 'e':
                continue            

        #The Normal mode (1) is the only one which do not
        #randomize the question-order.
        if mode == '1':
            setlist = range(dataset.datasize)
        else:
            setlist = [random.randrange(dataset.datasize)
                       for i in range(dataset.datasize)]
            
        #Question-Answer loop: loop through all sentences in dataset        
        ip = 1
        showAnswer = False
        while True:
            try:
                setnr = setlist[ip]

                #ask the question, and show the answer if told to
                prog.display_askSentence(ip,setnr,dataset)
                if showAnswer:
                    prog.display_answerSentence(setnr,dataset)
                    
                #display different footer menus and accept different
                #commands depending on which mode we are in

                #auto modes 
                if (not mode in ['1','2'] and not showAnswer) or \
                        mode in ['5','6']:              
                    timedelay = dataset.getPauseTime(setnr)
                    prog.display_autoMenu()
                    key = prog.waitForKey([K_c,K_m,K_e,K_f,K_s,K_h,
                                           K_n,K_l,K_SPACE],
                                          timedelay)                    
                    #if timer expired, key == None 
                    if not key:
                        if not showAnswer:
                            showAnswer = True                            
                            if mode in ['4','6'] or not prog.showlojban:
                                key = 'h'                            
                        elif mode in ['5','6']:
                            key = 'n'

                #interactive modes
                else:
                    prog.display_sentenceMenu()            
                    key = prog.waitForKey([K_c,K_p,K_n,K_h,
                                           K_e,K_l,K_SPACE])                                  

                #interpret the key given
                if key == 'e':
                    break 
                elif key == '?':
                    prog.display_helpMenu()
                    continue
                elif key == 'h':
                    showAnswer = True
                    sound.play(setnr,dataset)
                    continue
                elif key == 'c':                    
                    prog.showclock = not prog.showclock
                elif key == 'f':
                    prog.speedfactor -= 0.1
                elif key == 's':
                    prog.speedfactor += 0.1
                elif key == 'l':
                    prog.showlojban = not prog.showlojban
                elif key == 'n':
                    if ip == dataset.datasize-1 and showAnswer:
                        raise "Finished"                     
                    if showAnswer:
                        showAnswer = False
                        ip += 1
                    else:
                        showAnswer = True
                        if mode in ['4','6'] or not prog.showlojban:
                            sound.play(setnr,dataset)
                elif key == 'p':
                    ip -= 1
                    ip = max(1,ip)
                    showAnswer = False
                               
            except "NoSoundFile",e:
                s = ["This sound file could not be played:", str(e),
                     "Make sure the path is correct."]
                prog.errorMessage(s)
            except "Finished":
                s = ["You have finished this difficulty level!","",
                     "... Why not try another one?"]
                prog.errorMessage(s)
                break


if __name__=="__main__":
    main()
