#!/usr/bin/python

# file_monitor.py
#       --copyright--                   Copyright 2011 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       November 5, 2011        bar
#       November 19, 2011       bar     allow links on 1st level "files"
#                                       pause() and resume()
#       March 21, 2012          bar     comment
#       --eodstamps--
##      \file
#
#
#       This logic senses files changing by checking them every so often to see that they are the same date/time and size.
#
#       It senses changes to directories and sub-directories, too. It does not follow sym-links.
#
#

import  glob
import  math
import  os
import  threading
from    types                   import ListType, TupleType

import  tzlib


POLL_TIME   = math.e


class   a_file(object) :

    def all_files_in_dir(me, fn) :
        if  os.path.isdir(fn) and (me.link_ok or (not os.path.islink(fn))) :
            return([ a_file(fn) for fn in glob.glob(os.path.join(fn, "*")) if not os.path.basename(fn).startswith('.') ])
        return([])


    def __init__(me, fn, link_ok = False) :
        me.fn       = fn
        me.link_ok  = link_ok
        me.files    = me.all_files_in_dir(fn)
        me.restart()


    def changed_list(me) :
        fns         = []
        for f in me.files :
            fns    += f.changed_list()
        otm         = me.time
        osz         = me.size
        try         :
            me.time = os.path.getmtime(me.fn)
        except ( IOError, OSError ) :
            me.time = -2
        try         :
            me.size = os.path.getsize(me.fn)
        except ( IOError, OSError ) :
            me.size = -2
        if  ((me.time != otm) and (otm != -1)) or ((me.size != osz) and (osz != -1)) :
            fns.append(me.fn)

        afns        = tzlib.make_dictionary([ f.fn for f in me.all_files_in_dir(me.fn) ])

        for f in me.files :
            if  f.fn in afns :
                del(afns[f.fn])
            pass
        for fn in afns.keys() :
            f       = a_file(fn)
            f.removed()
            me.files.append(f)
            fns    += f.changed_list()

        return(fns)


    def restart(me) :
        for f in me.files :
            f.restart()
        me.time     = -1
        me.size     = -1


    def removed(me) :
        for f in me.files :
            f.removed()
        me.time     = -2
        me.size     = -2


    #   a_file



class   a_file_monitor(object) :

    #
    #   Note:   These callbacks are called from a thread.
    #
    def changed_now(me, fn) :                                       # stub for callback for when there is a change to the given file
        pass

    def changed(me, fn) :                                           # stub for callback to tell of file changes after some time of stability
        pass


    def __init__(me, files  = [], poll_time = None, on_changed_now = None, on_changed = None) :
        files               = files or []
        if  not isinstance(files, (ListType, TupleType)) :
            files           = [ files ]

        me.files            = [ a_file(fn, link_ok = True) for fn in files ]
        me.poll_time        = poll_time         or POLL_TIME
        me.on_changed_now   = on_changed_now    or me.changed_now           # called back immediately when a file change is sensed
        me.on_changed       = on_changed        or me.changed               # called back with all changes since the previous call to this callback - after a lag, a period of no-changes, so that multiply files can be changed before this callback is called.

        me.have_changed     = []
        me.lock             = threading.RLock()
        me._stop            = 0
        me._pause           = False
        me.th               = None


    def run(me) :
        me.lock.acquire()

        if  (not me._stop) and (not me._pause) :
            fns         = []
            for f in me.files :
                fns    += f.changed_list()

            if  fns     :
                me.on_changed_now(fns)
            elif len(me.have_changed) :                                     # wait for a poll that finds no new files before we call back to tell the owner what's up
                me.on_changed(tzlib.without_dupes(me.have_changed))
                me.have_changed = []
            me.have_changed    += fns

        me.lock.release()

        me.start()


    def start(me) :
        me.lock.acquire()
        if  not me._stop    :
            me.th           = threading.Timer(me.poll_time, me.run)
            me.th.setDaemon(True)
            me.th.start()
        me.lock.release()


    def restart(me)     :
        me.lock.acquire()
        for f in me.files :
            f.restart()
        me.lock.release()


    def pause(me)   :
        me._pause   = True
    def resume(me)  :
        me._pause   = False


    def stop(me)        :
        me.lock.acquire()
        (   me.th, th ) = ( None, me.th )
        me._stop        = 1
        me.lock.release()

        if  th          :
            try         :
                th.cancel()
            except RuntimeError :
                pass
            pass
        pass


    #   a_usb_sensor



if  __name__ == '__main__' :
    import  sys
    import  time

    def changed_now(fns) :
        print "Now:", fns

    def changed(fns) :
        print "Changed:", fns

    me  = a_file_monitor((sys.argv[1]   if len(sys.argv) > 1 else   '.'), on_changed_now = changed_now, on_changed = changed)
    me.start()

    while True :
        time.sleep(0.1)

    me.stop()


#
#
# eof

