# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function

from os import path
from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver
import inspect
import datetime as dt
import itertools
import sys
import time


class Monitor(object):
    """Monitoring your project files.

    **Parameters**

    :param basedir: The directory to start watching from.
        Defaults to `'.'`.

    **Attributes**

    :ivar observer: The watchdog.Observer instance.
    :ivar handlers: All the attached handlers.
    :ivar running: Boolean flag, to control the running while loop.

    **Basic Usage** ::

        #!/usr/bin/env python
        # -*- coding: utf-8 -*-
        # -*- filename: monitor.py

        from pandora import Monitor
        import logging

        monitor = Monitor()

        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)-15s %(levelname)-6s %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S')
        logger = logging.getLogger(__name__)


        @monitor.listen(['monitor.py'], on_startup=False)
        def reload(event=None, source=None):
            logger.info('Should reload the monitor.')
            monitor.restart_process()


        monitor.run()

    """

    def __init__(self, basedir='.', polling=False):
        """Initialize the Monitor."""
        if not polling:
            self.observer = Observer()
        else:
            self.observer = PollingObserver()

        self.handlers = []
        self.basedir = basedir
        self.running = False

    def stop(self):
        """Stop the observer manually."""
        self.running = False
        self.observer.stop()
        self.observer.join()

    def run(self, delay=1):
        """Start the observer and run the handlers when files change.

        This will catch the `KeyboardInterrupt` (`control-c`) exception. It
        will stop and join the observer process. After this your script
        continues.

        """
        self.observer.start()
        self.running = True

        tpl = 'Waiting for changes [{:4s}]'
        items = [tpl.format('.' * i) for i in range(1, 5)]

        spinner = itertools.cycle(items)

        try:
            while self.running:
                now = dt.datetime.now()
                sys.stdout.write(now.strftime('%H:%M:%S - '))
                sys.stdout.write(next(spinner))
                sys.stdout.flush()

                time.sleep(delay)

                sys.stdout.write('\b' * (len(items[0]) + 11))
                sys.stdout.flush()

                for handler in self.handlers:
                    handler.run()
        except KeyboardInterrupt:  # pragma: no cover
            self.observer.stop()
        self.observer.join()

    def listen(self, patterns, on_startup=False):
        """A decorator to attach a handler to the observer.

        The patterns are looped through, and the `basedir` is prepended
        to each pattern. If the patterns starts with an exclamation mark (!),
        the pattern is marked as an `exclude` pattern.

        **Parameters**

        :param patterns: A list of patterns to match files agains.
            Example: `['*.py', '*.ini', '!monitor.py']`.
        :param on_startup: Should the handler be ran as soon as the
            monitor is ran?
            Defaults to `False`.
        :type patterns: list
        :type on_startup: bool
        :return: A wrapper function, to be used as decorator.
        :rtype: callable
        """
        include = []
        exclude = []

        for pattern in patterns:
            if pattern[0] == '!':
                pattern = pattern[1:]
                into = exclude
            else:
                into = include

            into.append(path.join(self.basedir, pattern))

        if not len(include):
            include = None

        if not len(exclude):
            exclude = None

        def wrapper(callback):
            handler = Handler(include, exclude, callback, on_startup)

            self.handlers.append(handler)
            self.observer.schedule(handler, self.basedir, recursive=True)

            return callback

        return wrapper

    def restart_process(self):  # pragma: no cover
        """Restart the current python process.

        This is particulairy handy if your monitor `.py` file has changed. It
        simply calls :meth:`stop` and restarts itself.

        """
        from os import execl
        import sys

        self.stop()

        execl(sys.executable, sys.executable, *sys.argv)


class Handler(PatternMatchingEventHandler):
    """The actual `watchdog` event handler.

    The handler will run the attached callback on the `any_event`. This means
    that if `anything` happens to the file, the `must_run` flag will be set
    and the monitor will run the handler on the next iteration.

    By using the `must_run` flag and leting the monitor calling the handler
    on every `1 second` iteration, we make sure that the callback is not ran
    2 or 3 times in a row.

    **Parameters**

    :param include: Files matching this list are included.
        Example: `['*.py', '*.ini']`.
    :param exclude: Files matching this list are excluded.
        Example: `['*/__init__.py']`.
    :param callback: A callable that will be ran after the event.
    :param on_startup: Should the handler be ran as soon as the
        monitor is ran?
    :type include: list
    :type exclude: list
    :type callback: callable
    :type on_startup: bool

    **Attributes**

    :ivar must_run: A flag that will be set to `True` when the `any_event`
        is triggered by `watchdog`.

    """

    def __init__(self, include, exclude, callback, on_startup):
        """Initialize the Handler."""
        self.name = callback.__name__
        self.must_run = False
        self.changed_src_path = None
        self.changed_event = None
        self.callback = callback
        self.callback_args = inspect.getargspec(self.callback).args

        super(Handler, self).__init__(include, exclude)

        if on_startup:
            self.must_run = True

    def on_any_event(self, event):
        """Toggle the `must_run` attribute to `True`.

        **Parameters**

        :param event: The `watchdog` event.
        :type event: watchdog.events.FileSystemEvent
        """
        if not self.must_run:
            self.changed_src_path = event.src_path
            self.changed_event = event.event_type

        self.must_run = True

    def run(self):
        """Check the `must_run` attribute and executes the callback."""
        if self.must_run:
            if self.changed_src_path:
                now = dt.datetime.now()
                sys.stdout.write('{:s} - {:>15s} - {:7s} {}\n'.format(
                    now.strftime('%H:%M:%S'), self.callback.__name__,
                    self.changed_event,
                    self.changed_src_path))
                sys.stdout.flush()

            args = dict()

            if 'event' in self.callback_args:
                args['event'] = self.changed_event
            if 'source' in self.callback_args:
                args['source'] = self.changed_src_path

            self.callback(**args)
            self.must_run = False
            self.changed_src_path = None

    def __repr__(self):
        patterns = []

        if self._patterns:
            patterns.append(', '.join('"%s"' % p for p in
                                      self._patterns))
        if self._ignore_patterns:
            patterns.append(', '.join('"!%s"' % p for p in
                                      self._ignore_patterns))

        return '<Handler {:s} [{:s}]>'.format(
            self.name, ', '.join(patterns))
