import logging, re, os, fnmatch
from . import inotify 
log = logging.getLogger(__name__)

class FileWatcher(object):
    """Watches a directory for files matching fnmatch
    
    Assumes that all file-writes will be done with twrite,
    and by default assumes that ~ suffixed files should not 
    be processed (again, the twrite default temp-file suffix).
    
    .. doctest::
    
        >>> import os,shutil,tempfile
        >>> from fussy import twrite
        >>> temp = tempfile.mkdtemp(prefix='filewatcher-',suffix='-test')
        >>> with open(os.path.join(temp,'test.moo'),'wb') as fh:
        ...    written = fh.write(b'a')
        >>> f = FileWatcher( temp, recursive=True )
        >>> for event in f.start():
        ...    print(event) # doctest: +ELLIPSIS
        InotifyEvent(...,...,IN_CREATE,0,'.../filewatcher-...-test/test.moo')
        >>> with open(os.path.join(temp,'test2.txt'),'wb') as fh:
        ...    written = fh.write(b'a')
        >>> for event in f.iterate_events(None): # do not block
        ...    print(event) # doctest: +ELLIPSIS
        InotifyEvent(...,...,IN_CREATE,0,'.../filewatcher-...-test/test2.txt')
        >>> twrite.twrite( os.path.join(temp,'ieee','zoom'),'content')
        >>> for event in f.iterate_events(None): # do not block
        ...    print(event) # doctest: +ELLIPSIS
        InotifyEvent(...,...,IN_CREATE|IN_ISDIR,0,'.../filewatcher-...-test/ieee')
        InotifyEvent(...,...,IN_CREATE,0,'.../filewatcher-...-test/ieee/zoom')
        >>> shutil.rmtree( os.path.join(temp,'ieee'))
        >>> for event in f.iterate_events(None): # do not block
        ...    print(event) # doctest: +ELLIPSIS
        InotifyEvent(...,...,IN_DELETE,0,'.../filewatcher-...-test/ieee/zoom')
        InotifyEvent(...,...,IN_DELETE|IN_ISDIR,0,'.../filewatcher-...-test/ieee')
        >>> f.unwatch_directory( '/moo' ) # won't do anything, as it's not watched...
        >>> f.close()
        >>> f2 = FileWatcher(temp,pattern='*.moo',recursive=False)
        >>> for event in f2.start():
        ...    print(event) # doctest: +ELLIPSIS
        InotifyEvent(...,...,IN_CREATE,0,'.../filewatcher-...-test/test.moo')
        >>> shutil.rmtree(temp)
    
    """
    # maximum time before we iterate to avoid infinite hangs
    POLL_PERIOD = 20
    DEFAULT_EVENTS = (
        inotify.IN_MOVE|inotify.IN_MODIFY|
        inotify.IN_CREATE|inotify.IN_DELETE|
        inotify.IN_MOVED_TO|inotify.IN_MOVED_FROM
    )
    active = False
    def __init__(
        self,
        directory,
        pattern=re.compile(r'.+[^~]$'),
        events = None,
        recursive = False,
    ):
        """Watch directory for files matching pattern
        
        directory -- path to a directory
        pattern -- fnmatch string *or* regex object for matching
        """
        self.pattern = pattern
        self.directories = [directory]
        self.watches = {}
        self.back_watches = {}
        if events is None:
            events = self.DEFAULT_EVENTS
        self.events = events
        self.recursive = recursive
    
    def match(self, filename ):
        """Does this filename match our patterns?"""
        if hasattr(self.pattern,'match'):
            return self.pattern.match(filename)
        else:
            return fnmatch.fnmatchcase(os.path.basename(filename),self.pattern)
    
    _fd = None
    @property
    def fd(self):
        if self._fd is None:
            self._fd = inotify.Inotify()
        return self._fd
    def fileno(self):
        return self.fd
    
    def watch_directory(self,directory):
        if directory not in self.directories:
            self.directories.append(directory)
        if self.active and directory not in self.watches:
            log.info('Start watching: %s', directory)
            wd = self.watches[directory] = self.fd.add_watch(
                directory, 
                self.events,
            )
            log.debug('Watch: %s -> %s,%s',directory,int(self.fd),int(wd))
            return wd
    def unwatch_directory(self,directory,deleted=False):
        """De-register watches on given directory"""
        try:
            log.info('Stop watching: %s', directory)
            wd = self.watches.pop(directory)
        except KeyError:
            pass 
        else:
            try:
                self.fd.rm_watch(wd)
            except Exception as err:
                if hasattr(err,'errno') and err.errno == 22:
                    pass 
                elif not deleted:
                    log.warn('Failure during removal %s: %s', err,directory)
        while directory in self.directories:
            self.directories.remove(directory)
        prefix = directory + os.path.sep
        if prefix.endswith( os.path.sep *2 ):
            prefix = prefix[:-1]
        log.debug("Checking for prefix: %s",prefix)
        for subdir in [
            s for s in self.directories 
            if s.startswith( prefix )
        ]:
            self.unwatch_directory(subdir)
        
    def __iter__(self):
        log.info('Starting file watcher')
        try:
            for event in self.start():
                yield event
            while self.active:
                for event in self.iterate_events(self.POLL_PERIOD):
                    yield event
        finally:
            self.close()
    def start(self):
        """Start up our iterations
        
        yields create events for each matching file...
        """
        self.active = True
        for event in self.initial_watches():
            yield event
    def close(self):
        """Close our watches and shut down"""
        fd = self._fd 
        if fd:
            fd.close()
        self.watches.clear()
        self.active = False
        
    def initial_watches(self):
        """Walk our directories adding them to our watcher"""
        for directory in self.directories:
            for match in self.on_directory_added(directory):
                yield match
    def iterate_events(self,period=None):
        """Do a single iteration of polling for events on a given period"""
        events = self.fd.get_events(period)
        events = [
            e for e in events 
            if not (
                (e.mask & inotify.IN_IGNORED) or (e.mask & inotify.IN_DELETE_SELF)
            )
        ]
        for event in events:
            fullname = event.full_name
            if event.isdir:
                yield event
                if self.recursive:
                    if event.mask & inotify.IN_DELETE or event.mask & inotify.IN_MOVED_FROM:
                        self.on_directory_deleted( fullname )
                    elif event.mask & inotify.IN_CREATE or event.mask & inotify.IN_MOVED_TO:
                        for match in self.on_directory_added( fullname ):
                            yield match
            elif event.mask & inotify.IN_DELETE_SELF:
                log.debug('Watched directory removed: %s',fullname )
            elif self.match( fullname ):
                yield event
        
    
    def on_directory_deleted(self,directory):
        """A directory has been deleted"""
        self.unwatch_directory( directory, deleted=True )
    def on_directory_added(self,directory):
        """A directory has been created"""
        if self.recursive:
            for base,subdirs,files in os.walk(directory):
                wd = self.watch_directory( os.path.join(directory,base) )
                if wd:
                    for file in files:
                        fullname = os.path.join(base,file)
                        if self.match(fullname):
                            yield inotify.InotifyEvent(
                                fd = self.fd,
                                wd = wd,
                                mask=inotify.IN_CREATE,
                                cookie=0,
                                name=file,
                            )
        else:
            wd = self.watch_directory( directory )
            if wd:
                for file in os.listdir(directory):
                    fullname = os.path.join(directory,file)
                    if self.match(fullname):
                        yield inotify.InotifyEvent(
                            fd = self.fd,
                            wd = wd,
                            mask=inotify.IN_CREATE,
                            cookie=0,
                            name=file,
                        )

def options():
    """Create the options for demonstration watcher
    
    .. doctest::
    
        >>> parser = options()
        >>> args = parser.parse_args(['--recursive','/tmp/test'])
        >>> args.recursive
        True
        >>> args.target
        '/tmp/test'
    
    """
    import argparse
    parser = argparse.ArgumentParser(description='Watch directory for files matching given regex')
    parser.add_argument(
        '--recursive', action='store_true',
        default=False,
        help='Recursively watch all directories under the directory'
    )
    parser.add_argument(
        'target', metavar='DIR',
        help="Target directory to watch (must exist)",
    )
    return parser
    
def main():
    logging.basicConfig(level=logging.INFO)
    args = options().parse_args()
    r = FileWatcher( args.target, recursive=args.recursive, )
    for event in r:
        log.info('%r', event)
