#!/usr/bin/env python
# -*- coding: utf-8 mode: python -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8

u"""Afficher un chronomètre"""

import os, sys, re, subprocess, traceback
from os import path
from datetime import date as Date, time as Time
from datetime import datetime as Datetime, timedelta as Timedelta
from types import IntType, LongType

DEFAULT_SOUND = path.join(path.dirname(__file__), 'lib', 'chrono.wav')

def win_playSound(name):
    try: import winsound as s
    except: return
    if name is None:
        s.PlaySound(None, s.SND_ASYNC)
    else:
        scriptdir = path.split(path.abspath(sys.argv[0]))[0]
        soundfile = path.join(scriptdir, name)
        s.PlaySound(soundfile, s.SND_FILENAME + s.SND_ASYNC)

def linux_playSound(name):
    subprocess.call(['/usr/bin/aplay', '-Nq', name])

def playSound(name=None):
    if os.name == 'nt':
        return win_playSound(name)
    elif sys.platform.startswith('linux'):
        return linux_playSound(name)

def isnum(i):
    return type(i) in (IntType, LongType)
RE_NUM = re.compile(r'\d+$')
def numof(s):
    if isnum(s): return s
    elif s is None: return None
    elif RE_NUM.match(str(s)) is not None: return int(str(s))
    else: return s

DEFAULT_TIMEOUT = '5'

RE_DESTHOUR = re.compile(r'@(\d+)(?:[:.](\d+)(?:[:.](\d+))?)?$')
def parse_desthour(s):
    mo = RE_DESTHOUR.match(s)
    if mo is None: return None
    h, m, s = mo.groups()
    if h is None: h = 0
    if m is None: m = 0
    if s is None: s = 0
    h, m, s = int(h), int(m), int(s)
    src = Datetime.today()
    srcdate = src.date(); srctime = src.time()
    destdate = srcdate; desttime = Time(h, m, s)
    if desttime <= srctime: destdate = destdate + Timedelta(1)
    src = Datetime.combine(srcdate, srctime)
    dest = Datetime.combine(destdate, desttime)
    delta = dest - src
    return delta.total_seconds()

RE_TIMEOUT = re.compile(r'(\d+)(?:[:.](\d+)(?:[:.](\d+))?)?$')
def parse_timeout(s):
    mo = RE_TIMEOUT.match(s)
    if mo is None: return None
    h, m, s = mo.groups()
    if m is None and s is None:
        # M
        m = h
        h = None
    elif s is None:
        # M:S
        s = m
        m = h
        h = None
    else:
        # H:M:S
        pass
    if h is None: h = 0
    if m is None: m = 0
    if s is None: s = 0
    h, m, s = int(h), int(m), int(s)
    return h * 3600 + m * 60 + s

class Chrono:
    MODE_MINUTEUR = 'M'
    MODE_CHRONOMETRE = 'C'
    STATE_STARTED = 'started'
    STATE_PAUSED = 'paused'
    STATE_STOPPED = 'stopped'

    ZERO = None

    state = None
    mode = None
    timeout = None
    elapsed = None
    date_start = None
    max_elapsed = None
    initial = None

    def __init__(self, timeout=None, start=False):
        self.ZERO = self.__delta(0)
        self.set_timeout(timeout)
        if start: self.start()

    def __format(self, delta):
        h = delta.seconds // 3600
        seconds = delta.seconds % 3600
        m = seconds // 60
        s = seconds % 60
        if h > 0: return '%02i:%02i:%02i' % (h, m, s)
        else: return '%02i:%02i' % (m, s)

    def __delta(self, timeout):
        return Timedelta(seconds=timeout)

    def set_timeout(self, timeout=None):
        if timeout == '': timeout = None
        if timeout is not None and not isnum(timeout):
            tmp = parse_desthour(str(timeout))
            if tmp is None: tmp = parse_timeout(timeout)
            if tmp is None: tmp = int(timeout) * 60
            timeout = tmp
        if timeout == 0: timeout = None
        self.timeout = timeout
        self.elapsed = self.ZERO
        self.state = self.STATE_STOPPED
        if timeout is None:
            self.mode = self.MODE_CHRONOMETRE
            self.initial = self.__format(self.ZERO)
        else:
            self.mode = self.MODE_MINUTEUR
            self.max_elapsed = self.__delta(timeout)
            self.initial = self.__format(self.max_elapsed)

    def is_chronometre(self):
        return self.mode == self.MODE_CHRONOMETRE

    def is_minuteur(self):
        return self.mode == self.MODE_MINUTEUR

    def get_elapsed(self):
        if self.date_start is None: return self.ZERO
        delta = Datetime.today() - self.date_start
        return self.elapsed + delta

    def start(self, timeout=None):
        if timeout is None: timeout = self.timeout
        self.elapsed = self.ZERO
        self.date_start = Datetime.today()
        self.state = self.STATE_STARTED

    def is_started(self):
        return self.state == self.STATE_STARTED

    def pause(self):
        if self.state == self.STATE_PAUSED:
            self.date_start = Datetime.today()
            self.state = self.STATE_STARTED
        else:
            self.elapsed = self.get_elapsed()
            self.state = self.STATE_PAUSED

    def is_paused(self):
        return self.state == self.STATE_PAUSED

    def stop(self):
        self.elapsed = self.get_elapsed()
        self.state = self.STATE_STOPPED

    def is_stopped(self):
        return self.state == self.STATE_STOPPED

    def is_end(self):
        if not self.is_started(): return False
        if not self.is_minuteur(): return False
        elapsed = self.get_elapsed()
        return elapsed >= self.max_elapsed

    def __repr__(self):
        elapsed = self.get_elapsed() if self.is_started() else self.elapsed
        if self.is_minuteur():
            delta = self.max_elapsed - elapsed
            if delta < self.ZERO: delta = self.ZERO
            return self.__format(delta)
        else:
            return self.__format(elapsed)

def run_chronometre(timeout=None, autostart=False):
    from Tkinter import Tk, Toplevel, Frame, Label, Entry, Button
    import tkMessageBox

    class Dialog(Toplevel):
        def __init__(self, parent, title=None):
            self.result = None
            self.have_result = False

            Toplevel.__init__(self, parent)
            self.transient(parent)
            if title: self.title(title)
            self.parent = parent

            body = Frame(self)
            self.initial_focus = self.body(body)
            body.pack(padx=5, pady=5)
            self.buttonbox()
            self.grab_set()
            if not self.initial_focus: self.initial_focus = self
            self.protocol("WM_DELETE_WINDOW", self.cancel)
            self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                                      parent.winfo_rooty()+50))
            self.initial_focus.focus_set()
            self.wait_window(self)

        def set_result(self, result):
            self.result = result
            self.have_result = True

        def body(self, master):
            pass

        def buttonbox(self):
            box = Frame(self)
            w = Button(box, text="OK", width=10, command=self.ok, default='active')
            w.pack(side='left', padx=5, pady=5)
            w = Button(box, text="Annuler", width=10, command=self.cancel)
            w.pack(side='left', padx=5, pady=5)
            self.bind("<Return>", self.ok)
            self.bind("<Escape>", self.cancel)
            box.pack()

        def ok(self, event=None):
            if not self.validate():
                self.initial_focus.focus_set()
                return
            self.withdraw()
            self.update_idletasks()
            self.apply()
            self.cancel()

        def cancel(self, event=None):
            self.parent.focus_set()
            self.destroy()

        def validate(self):
            return True

        def apply(self):
            pass

    class Config(Dialog):
        def body(self, master):
            Label(master, text="Nb minutes", padx=20).grid(row=0)
            self.entry = Entry(master)
            self.entry.grid(row=0, column=1)
            return self.entry

        def apply(self):
            value = self.entry.get()
            if value == "": result = None
            else: result = value
            self.set_result(result)

    class Application(Frame):
        root = None
        chrono = None

        def __init__(self, timeout=None, autostart=False, **kw):
            self.chrono = Chrono(timeout)

            root = Tk()
            root.title("Chronomètre")
            root.columnconfigure(0, weight=1)
            root.rowconfigure(0, weight=1)
            root.bind("c", lambda event: self.do_config())
            root.bind("s", lambda event: self.do_start())
            root.bind("p", lambda event: self.do_pause())
            root.bind("<space>", lambda event: self.do_pause())
            root.bind("q", lambda event: self.quit())
            self.root = root

            kw.update(master=root)
            Frame.__init__(self, **kw)

            self.TIME = Label(self, width=10, height=2, text=self.chrono.initial, padx=30, pady=10, font=('Helvetica', 18, "normal"))
            self.START = Button(self, text="Start", command=self.do_start)
            self.PAUSE = Button(self, text="Pause", command=self.do_pause, state="disabled")
            self.CONFIG = Button(self, text="Config", command=self.do_config)
            self.QUIT = Button(self, text="Quit", command=self.quit)

            self.grid(column=0, row=0, sticky='nsew')
            self.TIME.grid(column=0, row=0, columnspan=4, sticky='nsew')
            self.START.grid(column=0, row=1, sticky='ew')
            self.PAUSE.grid(column=1, row=1, sticky='ew')
            self.CONFIG.grid(column=2, row=1, sticky='ew')
            self.QUIT.grid(column=3, row=1, sticky='ew')

            self.columnconfigure(0, weight=2)
            self.columnconfigure(1, weight=2)
            self.columnconfigure(2, weight=1)
            self.columnconfigure(3, weight=1)
            self.rowconfigure(0, weight=1)

            if autostart: self.do_start()

        def update_time(self):
            chrono = self.chrono
            self.TIME.configure(text=chrono)
            if chrono.is_started():
                if chrono.is_end():
                    playSound(DEFAULT_SOUND)
                else:
                    self.root.after(300, self.update_time)

        def do_start(self):
            self.PAUSE.configure(state="normal", text="Pause")
            self.START.configure(text="reStart")
            self.chrono.start()
            self.update_time()

        def do_pause(self):
            self.chrono.pause()
            if self.chrono.is_paused():
                self.PAUSE.configure(text="unPause")
            else:
                self.PAUSE.configure(text="Pause")
            self.update_time()

        def do_config(self):
            chrono = self.chrono
            chrono.stop()
            config = Config(self.root)
            if config.have_result:
                try:
                    self.PAUSE.configure(text="Pause", state="disabled")
                    self.START.configure(text="Start")
                    chrono.set_timeout(config.result)
                    self.TIME.configure(text=chrono.initial)
                except:
                    traceback.print_exc()
                    tkMessageBox.showerror("Valeur invalide", sys.exc_info()[1])

    Application(timeout, autostart).mainloop()

if __name__ == '__main__':
    from argparse import ArgumentParser, HelpFormatter
    if sys.argv[1:2] == ['--compat']:
        # Avec l'argument --compat, désactiver la classe FancyHelpFormatter qui
        # se base sur une API non documentée
        sys.argv = sys.argv[0:1] + sys.argv[2:]
        FancyHelpFormatter = HelpFormatter
    else:
        class FancyHelpFormatter(HelpFormatter):
            """Comme HelpFormatter, mais ne touche pas aux lignes qui commencent par les
            caractères '>>>'. Cela permet de mixer du texte formaté et du texte non
            formaté.
            """
            def _fill_text(self, text, width, indent):
                return ''.join([indent + line for line in text.splitlines(True)])
            def _split_lines(self, text, width):
                lines = ['']
                for line in text.splitlines():
                    if line.startswith('>>>'):
                        lines.append(line)
                        lines.append('')
                    else:
                        lines[-1] += '\n' + line
                lines = filter(None, lines)
                texts = []
                for line in lines:
                    if line.startswith('>>>'):
                        texts.append(line[3:])
                    else:
                        texts.extend(super(FancyHelpFormatter, self)._split_lines(line, width))
                return texts
    AP = ArgumentParser(
        usage=u"%(prog)s [options] [TIMEOUT]",
        description=u"Afficher un chronomètre",
        epilog=u"Si TIMEOUT est spécifié, par défaut le décompte démarre automatiquement.",
        formatter_class=FancyHelpFormatter,
    )
    AP.set_defaults(autostart=None, timeout=None)
    AP.add_argument('timeout', metavar='TIMEOUT', nargs='?',
        help=u"""\
>>>    '' (valeur vide)
chronomètre qui démarre à 0:00 et ne s'arrête pas
>>>    'H:M:S'    (heures:minutes:secondes)
>>> ou 'M:S'      (minutes:secondes)
>>> ou 'M'        (minutes)
minuteur qui démarre à H:M:S et fait un décompte jusqu'à 0:00. A la fin
du décompte, une sonnerie retentit.
>>>    '@H[:M[:S]]'
minuteur qui fonctionne comme précédemment, sauf qu'on spécifie l'heure
d'arrivée, et que la durée est calculée automatiquement""")
    AP.add_argument('-n', '--no-autostart', dest='autostart', action='store_false',
        help=u"Ne pas démarrer automatiquement le décompte même si TIMEOUT est spécifié.")
    AP.add_argument('-s', '--autostart', dest='autostart', action='store_true',
        help=u"Forcer le démarrage automatique du décompte, même si TIMEOUT n'est pas spécifié.")
    o = AP.parse_args()

    autostart = o.autostart
    if autostart is None: autostart = o.timeout is not None
    o.autostart = autostart

    timeout = o.timeout
    if timeout is None: timeout = DEFAULT_TIMEOUT
    o.timeout = timeout

    run_chronometre(o.timeout, o.autostart)
