#!/usr/bin/env python
# -*- coding: latin-1 -*-

# TeX-Units
# Équivalence des longueurs TeX
# (c) Olivier Pirson --- DragonSoft
# http://www.opimedia.be/DS/
# Débuté le 5 juillet 2006
# v.01.00 --- 14 juillet 2006
# v.01.01 --- 30 juillet 2006
# v.01.10 --- 10 août 2006
#         --- 21 septembre 2006
#         --- 2 mars 2008
#         --- 19 mai 2008
# v.01.11 --- 27 septembre 2009
# v.01.12 --- 15 mars 2010 : nouveau site web et adaptation Python 3
#         --- 2 janvier 2012 : nouveau site web
#####################################################################
from __future__ import print_function

VERSION = 'v.01.12 --- 2012 January 2'

import decimal, string, sys, time



##############
# Constantes #
##############
PREC = 200       # nombre de chiffres décimaux significatifs pour les calculs
PREC_PRINT = 12  # nombre de chiffres décimaux significatifs à afficher

PS_FILENAME = 'texUnits.eps'  # nom du fichier PostScript


UNITS_CONV = {}  # dictionnaire de tuple des fonctions de conversion
#                    vers sp et à partir de sp pour chaque unité
UNITS_CONV['pt'] = (lambda n: decimal.Decimal(n) * 65536,  # pt -> sp
                    lambda n: decimal.Decimal(n) / 65536)  # sp -> pt

UNITS_CONV['pc'] = (lambda n: UNITS_CONV['pt'][0](decimal.Decimal(n) * 12),  # pc -> sp
                    lambda n: UNITS_CONV['pt'][1](n) / 12)                   # sp -> pc

UNITS_CONV['in'] = (lambda n: UNITS_CONV['pt'][0](decimal.Decimal(n) * decimal.Decimal('72.27')),  # in -> sp
                    lambda n: UNITS_CONV['pt'][1](n) / decimal.Decimal('72.27'))                   # sp -> in

UNITS_CONV['bp'] = (lambda n: UNITS_CONV['in'][0](decimal.Decimal(n) / 72),  # bp -> sp
                    lambda n: UNITS_CONV['in'][1](n) * 72)                   # sp -> bp

UNITS_CONV['cm'] = (lambda n: UNITS_CONV['in'][0](decimal.Decimal(n) / decimal.Decimal('2.54')),  # cm -> sp
                    lambda n: UNITS_CONV['in'][1](n) * decimal.Decimal('2.54'))                   # sp -> cm

UNITS_CONV['mm'] = (lambda n: UNITS_CONV['cm'][0](decimal.Decimal(n) / 10),  # mm -> sp
                    lambda n: UNITS_CONV['cm'][1](n) * 10)                   # sp -> mm

UNITS_CONV['dd'] = (lambda n: UNITS_CONV['pt'][0](decimal.Decimal(n) * 1238 / 1157),  # dd -> sp
                    lambda n: UNITS_CONV['pt'][1](n) / 1238 * 1157)                   # sp -> dd

UNITS_CONV['cc'] = (lambda n: UNITS_CONV['dd'][0](decimal.Decimal(n) * 12),  # cc -> sp
                    lambda n: UNITS_CONV['dd'][1](n) / 12)                   # sp -> cc

UNITS_CONV['sp'] = (lambda n: decimal.Decimal(n),  # sp -> sp
                    lambda n: decimal.Decimal(n))  # sp -> sp



#############
# Fonctions #
#############
def decimal_to_str(n, prec=-1):
    """Renvoie n sous forme de string
    avec au plus prec chiffres significatifs
    pre: n: Decimal >= 0
         prec: naturel ou -1"""
    if prec >= 0:
        n = n.quantize(decimal.Decimal('.' + '0' * prec))
    n = n.normalize().as_tuple()
    e = n[2]  # exposant
    n = n[1]  # liste non vide des chiffres

    l = []  # liste des chiffres sous forme de string
    if e <= 0:  # partie entière sans exposant
        for i in range(0, len(n) + e):
            l.append(str(n[i]))
        if e < 0:  # partie décimale
            l.append('.')
            for i in range(len(n) + e, len(n)):
                l.append(str(n[i]))
    else:  # partie entière avec exposant
        for i in range(0, len(n)):
            l.append(str(n[i]))
        l.append('0' * e)
    return ''.join(l)


def gui_tk(d, l, round_sp):
    """Front-end Tk
    Si round_sp
    alors les longueurs sont arrondies à l'unité sp la plus proche
    pre: d: dictionnaire contenant les longueurs pour chaque unité
         l: liste des longueurs à traiter pour le fichier PostScript
         round_sp: boolean"""
    import os

    if sys.version_info[0] >= 3:  # Python >= 3
        import tkinter as tk
        import tkinter.messagebox as tkMessageBox
    else:                         # 2.6 <= Python < 3
        import Tkinter as tk
        import tkMessageBox

    w = tk.Tk()  # fenêtre principale
    w.title('TeX-Units')
    w.resizable(0,0)

    e = {}  # dictionnaire des widgets Entry pour chaque unité

    # Fonctions associées aux boutons
    def cmd_about():
        'Message about...'
        t = tkMessageBox.showinfo('About' + chr(8230),
                                  """TeX-Units

{0}
(c) Olivier Pirson --- DragonSoft
http://www.opimedia.be/DS/


Infos:
Python {1}""".format(VERSION, sys.version))

    def cmd_clear_all():
        """Efface le fichier PostScript, vide la liste des longueurs à traitées
        et les widgets Entry"""
        cmd_clear_ps()
        for u in d:
            e[u].delete(0,tk.END)
        e['all'].delete(0,tk.END)

    def cmd_clear_ps():
        """Efface le fichier PostScript et vide la liste des longueurs à traitées"""
        del(l[:])
        if os.path.isfile(PS_FILENAME):
            try:
                os.remove(PS_FILENAME)
            except:
                print('Failed remove {0!r}!!!'.format(PS_FILENAME), file=sys.stderr)

    def cmd_make_ps():
        """Crée le fichier PostScript"""
        make_ps(l, bool(opt_round.get()))


    def updates(s, u):
        """Update les valeurs des Entry à partir de la longueur s d'unité u
        Si s n'est pas valide le Entry correspondant à u est grisé
        pre: s: string
             u: 'all' ou une des unités gérées"""
        if u != 'all':
            s += u

        try:
            d = make_results(s, bool(opt_round.get()))
        except:
            e[u].config(bg = 'grey')
        else:
            if s != '' and (len(l) == 0 or l[-1] != s):  # s est une nouvelle valeur
                l.append(s)
            if u != 'all':  # modifie l'Entry 'all' en fonction de s
                e['all'].delete(0, tk.END)
                e['all'].insert(0, s)
            e['all'].config(bg = 'white')
            # modifie les Entry pour chaque unité
            for u in d:
                e[u].config(bg = 'white')
                e[u].delete(0, tk.END)
                e[u].insert(0, d[u])

    def updates_last():
        """Update les valeurs des Entry à partir de l'éventuelle dernière longueur"""
        updates(l[-1] if len(l) > 0
                else '',
                'all')


    # Crée les widgets Entry pour chaque unité avec les Label associés
    i = 0  # numéro de ligne pour grid()
    for t in [('pt', '(point)', '1pt = 65536sp', lambda event: updates(event.widget.get(), 'pt')),
              None,
              ('in', '(inch)', '1in = 72.27pt', lambda event: updates(event.widget.get(), 'in')),
              None,
              ('cm', '(centimeter)', '2.54cm = 1in', lambda event: updates(event.widget.get(), 'cm')),
              ('mm', '(millimeter)', '10mm = 1cm', lambda event: updates(event.widget.get(), 'mm')),
              None,
              ('bp', '(big point)', '72bp = 1in', lambda event: updates(event.widget.get(), 'bp')),
              ('cc', '(cicero)', '1cc = 12dd', lambda event: updates(event.widget.get(), 'cc')),
              ('dd', '(didot point)', '1157dd = 1238pt', lambda event: updates(event.widget.get(), 'dd')),
              ('pc', '(pica)', '1pc = 12pt', lambda event: updates(event.widget.get(), 'pc')),
              ('sp', '(scaled point)', 'primitive unit of TeX', lambda event: updates(event.widget.get(), 'sp'))]:
        if t != None:
            u = t[0]
            e[u] = tk.Entry()
            e[u].bind('<Return>', t[3])
            e[u].grid()

            tk.Label(w, text=u).grid(row=i, column=1, sticky=tk.W)

            if u != 'sp':
                tk.Label(w, text=t[1]).grid(row=i, column=2, sticky=tk.W)
            else:
                f = tk.Frame(w)
                tk.Label(f, text=t[1]).pack(side='left')
                opt_round = tk.IntVar()
                opt_round.set(round_sp)
                b = tk.Checkbutton(f, text='round', variable=opt_round, command=updates_last)
                b.pack(side='left')
                f.grid(row=i, column=2, sticky=tk.W)

            tk.Label(w, text=t[2]).grid(row=i, column=3, padx=10, sticky=tk.W)
        else:  # espacement
            tk.Frame(w, height=12).grid(row=i, sticky=tk.W)
        i += 1

    tk.Frame(w, height=8).grid(row=i, sticky=tk.W)

    e['all'] = tk.Entry()
    e['all'].bind('<Return>', lambda event: updates(event.widget.get(), 'all'))
    if len(l) > 0:
        e['all'].insert(tk.INSERT, l[-1])
    e['all'].grid(row=i+1, column=0)
    e['all'].focus_set()

    f = tk.Frame(w)
    tk.Button(f, text='PostScript', command=cmd_make_ps).pack(side='left', padx=10)
    tk.Button(f, text='Clear PS', command=cmd_clear_ps).pack(side='left')
    tk.Button(f, text='Clear all', command=cmd_clear_all).pack(side='left')
    tk.Button(f, text='About', command=cmd_about).pack(side='left', padx=10)
    f.grid(row=i+1, column=1, columnspan=3)

    tk.Frame(w, height=2).grid(row=i+2, sticky=tk.W)

    updates_last()
    w.mainloop()


def help_msg():
    """Affiche le message d'aide sur la sortie des erreurs
    et termine le programme par un code d'erreur 1"""
    print("""texUnits [PS] [round] [Tk] [length...]

  TeX-Units: equivalent TeX' lengths
  (c) Olivier Pirson --- DragonSoft --- http://www.opimedia.be/DS/
             {0}
  Options: PS:    make encapsulated PostScript file 'texUnits.eps'
           round: round the sp unit (like TeX)
           Tk:    front-end Tk
  length...: lengths in TeX units:bp, cc, cm, dd, in, mm, pc, pt(default) or sp
             (' ' and '\\t' are deleted; ',' = '.' the decimal point)""".format(VERSION),
          file=sys.stderr)
    sys.exit(1)


def make_ps(l, round_sp, filename=PS_FILENAME):
    """Création d'un fichier encapsulated PostScript
    représentant les longueurs et les différents unités
    Si round_sp
    alors les longueurs sont arrondies à l'unité sp la plus proche
    pre: l: liste de chaîne de caractères contenant les longueurs à traiter
            en plus des différentes unités
         round_sp: boolean
         filename: nom du fichier"""
    try:
        f = open(filename, 'w')
    except IOError:
        print("Failed open 'index.htm' for writing!!!", file=sys.stderr)
        return
    print("""%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 750 {0}
%%Title: texUnits.eps
%%Creator: TeX-Units --- {1}
%%CreationDate: {2}
%%Pages: 1
%%EndComments
%%Page: 1 1
%%BeginDocument: texUnits.eps

/Times-Roman findfont
10 scalefont
setfont
""".format(115 + len(l)*10, VERSION, time.strftime('%Y-%m-%d')),
          file=f)

    l_bp = []  # liste des longueurs traitées en unité bp

    print('% Lignes horizontales des longueurs traitees', file=f)
    for i in range(len(l)):
        d = make_results(l[i], round_sp)
        l_bp.append(d['bp'])
        print("""25 {0} moveto
{1} 0 rlineto
""".format((105 + (len(l) - i) * 10), l_bp[i]),
              file=f)

    print('% Lignes horizontales des longueurs unites (en noir) et des longueurs unites * 10 (en bleu)',
          file=f)
    coefs = (1, 10)
    for i in range(2):
        print('25 {0} moveto {1} 0 rlineto'.format(100 + i, texUnits_conv(coefs[i], 'pt', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(85 + i, texUnits_conv(coefs[i], 'in', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(70 + i, texUnits_conv(coefs[i], 'cm', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(60 + i, texUnits_conv(coefs[i], 'mm', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(45 + i, coefs[i]), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(35 + i, texUnits_conv(coefs[i], 'cc', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(25 + i, texUnits_conv(coefs[i], 'dd', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(15 + i, texUnits_conv(coefs[i], 'pc', 'bp')), file=f)
        print('25 {0} moveto {1} 0 rlineto'.format(5 + i, texUnits_conv(coefs[i], 'sp', 'bp')), file=f)
        if i == 0:
            print("""stroke

.3 .3 .8 setrgbcolor""",
                  file=f)
    print("""stroke
0 setgray
""",
          file=f)

    print('% Etiquette des longueurs traitees', file=f)
    for i in range(len(l)):
        s = l[i]
        if s[-1] in string.digits:
            unit = ''
        else:
            unit = s[-2:]
            s = s[:-2]
        print('{0} {1} moveto ({2}{3}) show'.format(l_bp[i]+30, 105 + (len(l) - i) * 10, s, unit), file=f)

    print("""
% Etiquette des longueurs unites
1 100 moveto (1pt) show
1 85 moveto (1in) show
1 70 moveto (1cm) show
1 60 moveto (1mm) show
1 45 moveto (1bp) show
1 35 moveto (1cc) show
1 25 moveto (1dd) show
1 15 moveto (1pc) show
1 5 moveto (1sp) show

showpage
%%EndDocument
%%Trailer""",
          file=f)
    f.close()


def make_results(s, round_sp):
    """Renvoie un dictionnaire de Decimal
    contenant l'équivalent de la longueur s
    pour chaque unité : bp, cc, cm, dd, in, mm, pc, pt et sp
    Si round_sp alors la longueur est arrondie à l'unité sp la plus proche
      (sp est l'unité ENTIÈRE primitive de TeX)
    pre: s: chaîne de caractères de la longueur avec son unité
              (pt par défaut)
              (Les ' ' \\t sont effacés, les ',' remplacés par des '.')
         round_sp: boolean
    exception: transmet les exceptions
               si la conversion à partir de s se passe mal"""
    s = s.replace(' ', '').replace('\t', '').replace(',', '.').lower()
    # Conversion ? -> sp
    if len(s) >= 2 and s[-1] in string.ascii_lowercase:  # une unité est spécifiée
        u = s[-2:]
        s = s[:-2]
    else:
        u = 'pt'
    sp = UNITS_CONV[u][0](decimal.Decimal('0{0}'.format(s)))

    if round_sp:
        sp = sp.quantize(decimal.Decimal(0))

    # Calcule les longueurs dans chaque unité : sp -> ...
    d = {}
    for u in UNITS_CONV:
        d[u] = UNITS_CONV[u][1](sp).normalize() + 0
    return d


def print_results(d):
    """Envoie les résultats de d sur la sortie standard
    pre: d: dictionnaire de Decimal
            contenant les longueurs pour chaque unité"""
    # Construit les string à afficher
    d_s = {}  # dictionnaire de (string, string) justifiées pour chaque unités
    l_int = 0  # plus grand nombre de chiffres pour la partie entière
    l_frac = 0  # plus grand nombre de chiffres pour la partie décimale
    for u in d:
        s_int = decimal_to_str(d[u], PREC_PRINT).split('.')
        if len(s_int) == 1:  # pas de partie décimale
            s_frac = ''
            s_int = s_int[0]
        else:
            s_frac = s_int[1]
            s_int = s_int[0]
        d_s[u] = (s_int, s_frac)
        l_int = max(l_int, len(s_int))
        l_frac = max(l_frac, len(s_frac))

    # Accole les string de d_s et les justifie
    for u in d:
        if len(d_s[u][1]) > 0:
            d_s[u] = '{0}{1}.{2}{3}'.format(' ' * (l_int - len(d_s[u][0])), d_s[u][0],
                                            d_s[u][1], ' ' * (l_frac - len(d_s[u][1])))
        else:
            d_s[u] = '{0}{1} {2}'.format(' ' * (l_int - len(d_s[u][0])), d_s[u][0],
                                         ' ' * l_frac)

    # Affichage
    print('{0} pt (point)          (1pt = 65536sp)'.format(d_s['pt']))
    print()
    print('{0} in (inch)           (1in = 72.27pt)'.format(d_s['in']))
    print()
    print('{0} cm (centimeter)     (2.54cm = 1in)'.format(d_s['cm']))
    print('{0} mm (millimeter)     (10mm = 1cm)'  .format(d_s['mm']))
    print()
    print('{0} bp (big point)      (72bp = 1in)'           .format(d_s['bp']))
    print('{0} cc (cicero)         (1cc = 12dd)'           .format(d_s['cc']))
    print('{0} dd (didot point)    (1157dd = 1238pt)'      .format(d_s['dd']))
    print('{0} pc (pica)           (1pc = 12pt)'           .format(d_s['pc']))
    print('{0} sp (scaled point)   (primitive unit of TeX)'.format(d_s['sp']))


def texUnits_conv(n, src='sp', dest='sp'):
    """Renvoie la conversion de n (unité de longueur exprimée en unité src)
    dans l'unité dest
    pre: n: Decimal"""
    return UNITS_CONV[dest][1](UNITS_CONV[src][0](n))



########
# Main #
########
if __name__ == '__main__':
    if len(sys.argv) == 1:
        help_msg()

    decimal.getcontext().prec = PREC

    opt_round = False # option round : arrondi l'unité sp à l'unité entière la plus proche
    opt_tk = False  # option Tk
    l = []  # liste des longueurs à traiter
    d = make_results('1pt', opt_round)  # dictionnaire contenant les longueurs pour chaque unité

    # Parcours les paramètres
    for i in range(1, len(sys.argv)):
        if sys.argv[i].lower() == 'ps':
            make_ps(l, opt_round)
        elif sys.argv[i].lower() == 'round':
            opt_round = True
        elif sys.argv[i].lower() == 'tk':
            opt_tk = True
        else:
            if len(l) > 0:  # pas la 1ère longueur
                print()
            s = sys.argv[i]
            l.append(s)
            try:
                d = make_results(s, opt_round)
            except:
                help_msg()
            print('---', s, '---')
            print_results(d)

    if opt_tk:
        gui_tk(d, l, opt_round)
