#!/usr/bin/python3
'''A set of convenience functions for converting among different phone codes.
   Usage: 
   import phonecodes
   print phonecodes.CODES   # the known phone codes
   print phonecodes.LANGUAGES # the known languages
   s1 = phonecodes.convert(s0, code0, code1, language)
   # s0 and s1 are strings containing individual symbols
   # code0 and code1 must be members of phonecodes.CODES, of course
   # language must be a member of phonecodes.LANGUAGES, of course
   #   (but not all languages are known for all phone codes)
   l1 = phonecodes.convertlist(l0, code0, code1, language)
   # l0, l1 are lists of symbols
   phonecodes.vowels
   phonecodes.consonants
   # list known IPA symbols of vowels, consonants.
   # for other tables, see phondler_tables.py
'''
import re,sys
import phodic.phond_tables as phodic_tables

CODES=set(('ipa','arpabet','xsampa','disc','callhome'))
LANGUAGES=set(('eng','deu','nld','arz','cmn','spa','yue','lao','vie'))

vowels = phodic_tables._ipa_vowels
consonants = phodic_tables._ipa_consonants

#####################################################################
def translate_string(s, d):
    '''(tl,ttf)=translate_string(s,d):
    Translate the string, s, using symbols from dict, d, as:
    1. Min # untranslatable symbols, then 2. Min # symbols.
    tl = list of translated or untranslated symbols.
    ttf[n] = True if tl[n] was translated, else ttf[n]=False.
'''
    N = len(s)
    symcost = 1    # path cost per translated symbol
    oovcost = 10   # path cost per untranslatable symbol
    maxsym = max(len(k) for k in d.keys())  # max input symbol length
    # (pathcost to s[(n-m):n], n-m, translation[s[(n-m):m]], True/False)
    lattice = [ (0,0,'',True) ]
    for n in range(1,N+1):
        # Initialize on the assumption that s[n-1] is untranslatable
        lattice.append((oovcost+lattice[n-1][0],n-1,s[(n-1):n],False))
        # Search for translatable sequences s[(n-m):n], and keep the best
        for m in range(1,min(n+1,maxsym+1)):
            if s[(n-m):n] in d and symcost+lattice[n-m][0] < lattice[n][0]:
                lattice[n] = (symcost+lattice[n-m][0],n-m,d[s[(n-m):n]],True)
    # Back-trace
    tl = []
    translated = []
    n = N
    while n > 0:
        tl.append(lattice[n][2])
        translated.append(lattice[n][3])
        n = lattice[n][1]
    return((tl[::-1], translated[::-1]))

def attach_tones_to_vowels(il, tones, vowels, searchstep, catdir):
    '''Return a copy of il, with each tone attached to nearest vowel if any.
    searchstep=1 means search for next vowel, searchstep=-1 means prev vowel.
    catdir>=0 means concatenate after vowel, catdir<0 means cat before vowel.
    Tones are not combined, except those also included in the vowels set.
    '''
    ol = il.copy()
    v = 0 if searchstep>0 else len(ol)-1
    t = -1
    while 0<=v and v<len(ol):
        if (ol[v] in vowels or (len(ol[v])>1 and ol[v][0] in vowels)) and t>=0:
            ol[v]= ol[v]+ol[t] if catdir>=0 else ol[t]+ol[v]
            ol = ol[0:t] + ol[(t+1):]  # Remove the tone
            t = -1 # Done with that tone
        if v<len(ol) and ol[v] in tones:
            t = v
        v += searchstep
    return(ol)

#####################################################################
# X-SAMPA
def ipa2xsampa(x,language):
    '''Attempt to return X-SAMPA equivalent of an IPA phone x.'''
    (tl,ttf) = translate_string(x, phodic_tables._ipa2xsampa)
    return(''.join(tl))

def xsampa2ipa(x,language):
    '''Return the IPA equivalent of X-SAMPA phone x.'''
    (tl,ttf) = translate_string(x, phodic_tables._xsampa_and_diac2ipa)
    return(''.join(tl))
    
######################################################################
# Language-dependent lexical tones and stress markers
def tone2ipa(n, language):
    return(phodic_tables._tone2ipa[L][int(n[1:])])

#####################################################################
# DISC, the system used by CELEX
def disc2ipa(x, L):
    '''Convert DISC symbol x into IPA, for language L'''
    if L=='nld':
        (tl,ttf) = translate_string(x, phodic_tables._disc2ipa_dutch)
        return(''.join(tl))
    elif L=='eng':
        (tl,ttf) = translate_string(x, phodic_tables._disc2ipa_english)
        return(''.join(tl))
    else:
        (tl,ttf) = translate_string(x, phodic_tables._disc2ipa)
        return(''.join(tl))

def ipa2disc(x,L):
    '''Convert IPA symbol x into DISC'''
    (tl,ttf) = translate_string(x, phodic_tables._ipa2disc)
    return(''.join(tl))

def ipa2disc_old(x,L):
    '''Convert IPA symbol x into DISC, for language L'''
    # Convert whole thing if possible; otherwise try prefix+vowel; else quit
    if x in phodic_tables._ipa2disc:
        return(phodic_tables._ipa2disc[x])
    elif x[0] in phodic_tables._ipa2disc and x[1:] in phodic_tables._ipa2disc:
        return(phodic_tables._ipa2disc[x[0]] + phodic_tables._ipa2disc[x[1:]])
    else:
        raise KeyError('Unknown IPA symbol %s for language %s'%(x,L))

#######################################################################
# Callhome phone codes
def callhome2ipa(x,L):
    '''Convert callhome phone symbol x into IPA for language L'''
    (il,ttf)=translate_string(x, phodic_tables._callhome2ipa[L])
    if L=='arz':
        ol = attach_tones_to_vowels(il, phodic_tables._ipa_stressmarkers,
                                    phodic_tables._ipa_vowels, -1, -1)
    elif L=='cmn':
        ol=attach_tones_to_vowels(il, phodic_tables._ipa_tones,
                                  phodic_tables._ipa_vowels, -1, 1)
    elif L=='spa':
        ol=attach_tones_to_vowels(il, phodic_tables._ipa_stressmarkers,
                                  phodic_tables._ipa_vowels, -1, -1)
    return(''.join(ol))

def ipa2callhome(x,L):
    '''Convert IPA symbol x into callhome notation, for language L'''
    (il,ttf)=translate_string(x, phodic_tables._ipa2callhome[L])
    if L=='arz':
        ol=attach_tones_to_vowels(il,'012', phodic_tables._callhome_vowels['arz'], 1, 1)
    elif L=='cmn':
        ol=attach_tones_to_vowels(il,'012345', phodic_tables._callhome_vowels['cmn'], -1, 1)
    elif L=='spa':
        ol=attach_tones_to_vowels(il,'012', phodic_tables._callhome_vowels['spa'], 1, 1)
    return(''.join(ol))

#########################################################################
# ARPABET and TIMIT
def arpabet2ipa(x,language):
    '''Convert ARPABET symbol X to IPA'''
    (il,ttf)=translate_string(x, phodic_tables._arpabet2ipa)
    ol=attach_tones_to_vowels(il, phodic_tables._ipa_stressmarkers,
                              phodic_tables._ipa_vowels, -1, -1)
    return(''.join(ol))

def ipa2arpabet(x,language):
    '''Convert IPA symbols to ARPABET'''
    (il,ttf)=translate_string(x, phodic_tables._ipa2arpabet)
    ol=attach_tones_to_vowels(il,'012', phodic_tables._arpabet_vowels, 1, 1)
    return(''.join(ol))

def timit2ipa(x,L):
    '''Convert TIMIT phone codes to IPA'''
    x = x.upper()
    (il,ttf)=translate_string(x, phodic_tables._timit2ipa)
    ol=attach_tones_to_vowels(il, phodic_tables._ipa_stressmarkers,
                              phodic_tables._ipa_vowels, -1, -1)
    return(''.join(ol))

#######################################################################
# phonecodes.convert and phonecodes.convertlist
# are used to convert symbols and lists of symbols, respectively,
# to or from IPA, by calling appropriate other functions.
#
_convertfuncs = {
    'arpabet': (arpabet2ipa, ipa2arpabet),
    'xsampa': (xsampa2ipa, ipa2xsampa),
    'disc': (disc2ipa, ipa2disc),
    'callhome': (callhome2ipa,ipa2callhome)
}
def convert(s0, c0, c1, language):
    if c0=='ipa' and c1!='ipa':
        x=_convertfuncs[c1][1](s0, language)
        return(x)
    elif c0!='ipa' and c1=='ipa':
        return(_convertfuncs[c0][0](s0, language))
    else:
        raise RuntimeError('must convert to/from ipa, not %s to %s'%(c0,c1))

def convertlist(l0, c0, c1, language):
    return([ convert(s0,c0,c1,language) for s0 in l0 ])

