# -*- coding: utf-8 -*-
"""
    livereload.watcher
    ~~~~~~~~~~~~~~~~~~

    A file watch management for LiveReload Server.

    :copyright: (c) 2013 - 2015 by Hsiaoming Yang
    :license: BSD, see LICENSE for more details.
"""

import os
import glob
import time
try:
    import pyinotify
except ImportError:
    pyinotify = None


class Watcher(object):
    """A file watcher registery."""
    def __init__(self):
        self._tasks = {}
        self._mtimes = {}

        # setting changes
        self._changes = []

        # filepath that is changed
        self.filepath = None
        self._start = time.time()

        # ignored file extensions
        self.ignored_file_extensions = ['.pyc', '.pyo', '.o', '.swp']

    def should_ignore(self, filename):
        """Should ignore a given filename?"""
        _, ext = os.path.splitext(filename)
        return ext in self.ignored_file_extensions

    def ignore_file_extension(self, extension):
        self.ignored_file_extensions.append(extension)

    def watch(self, path, func=None, delay=0, ignore=None):
        """Add a task to watcher.

        :param path: a filepath or directory path or glob pattern
        :param func: the function to be executed when file changed
        :param delay: Delay sending the reload message. Use 'forever' to
                      not send it. This is useful to compile sass files to
                      css, but reload on changed css files then only.
        :param ignore: A function return True to ignore a certain pattern of
                       filepath.
        """
        self._tasks[path] = {
            'func': func,
            'delay': delay,
            'ignore': ignore,
        }

    def start(self, callback):
        """Start the watcher running, calling callback when changes are
        observed. If this returns False, regular polling will be used."""
        return False

    def examine(self):
        """Check if there are changes, if true, run the given task."""
        if self._changes:
            return self._changes.pop()

        # clean filepath
        self.filepath = None
        delays = set([0])
        for path in self._tasks:
            item = self._tasks[path]
            if self.is_changed(path, item['ignore']):
                func = item['func']
                func and func()
                delay = item['delay']
                if delay:
                    delays.add(delay)

        if 'forever' in delays:
            delay = 'forever'
        else:
            delay = max(delays)
        return self.filepath, delay

    def is_changed(self, path, ignore=None):
        if isinstance(path, (list, tuple)):
            path = path[1]
        if os.path.isfile(path):
            return self.is_file_changed(path, ignore)
        elif os.path.isdir(path):
            return self.is_folder_changed(path, ignore)
        return self.is_glob_changed(path, ignore)

    def is_file_changed(self, path, ignore=None):
        if not os.path.isfile(path):
            return False

        if self.should_ignore(path):
            return False

        if ignore and ignore(path):
            return False

        mtime = os.path.getmtime(path)

        if path not in self._mtimes:
            self._mtimes[path] = mtime
            self.filepath = path
            return mtime > self._start

        if self._mtimes[path] != mtime:
            self._mtimes[path] = mtime
            self.filepath = path
            return True

        self._mtimes[path] = mtime
        return False

    def is_folder_changed(self, path, ignore=None):
        for root, dirs, files in os.walk(path, followlinks=True):
            if '.git' in dirs:
                dirs.remove('.git')
            if '.hg' in dirs:
                dirs.remove('.hg')
            if '.svn' in dirs:
                dirs.remove('.svn')
            if '.cvs' in dirs:
                dirs.remove('.cvs')

            for f in files:
                if self.is_file_changed(os.path.join(root, f), ignore):
                    return True
        return False

    def is_glob_changed(self, path, ignore=None):
        for f in glob.glob(path):
            if self.is_file_changed(f, ignore):
                return True
        return False


class INotifyWatcher(Watcher):
    def __init__(self):
        Watcher.__init__(self)

        self.wm = pyinotify.WatchManager()
        self.notifier = None
        self.callback = None

    def watch(self, path, func=None, delay=None, ignore=None):
        flag = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
        self.wm.add_watch(path, flag, rec=True, do_glob=True, auto_add=True)
        Watcher.watch(self, path, func, delay, ignore)

    def inotify_event(self, event):
        self.callback()

    def start(self, callback):
        if not self.notifier:
            self.callback = callback

            from tornado import ioloop
            self.notifier = pyinotify.TornadoAsyncNotifier(
                self.wm, ioloop.IOLoop.instance(),
                default_proc_fun=self.inotify_event
            )
            callback()
        return True


def get_watcher_class():
    if pyinotify is None:
        return Watcher
    return INotifyWatcher
