#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Monitors a directory tree for changes using win32 APIs."""


import ctypes
import os

# FindNextChangeNotification constants (defined in FileAPI.h):
FILE_NOTIFY_CHANGE_FILE_NAME = 0x00000001
FILE_NOTIFY_CHANGE_DIR_NAME = 0x00000002
FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x00000004
FILE_NOTIFY_CHANGE_SIZE = 0x00000008
FILE_NOTIFY_CHANGE_LAST_WRITE = 0x00000010
FILE_NOTIFY_CHANGE_CREATION = 0x00000040
FILE_NOTIFY_CHANGE_SECURITY = 0x00000100

# WaitForSingleObject return values (defined in WinBase.h):
WAIT_OBJECT_0 = 0x00000000L
WAIT_TIMEOUT = 0x00000102L
WAIT_FAILED = 0xffffffff

INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value

_INTERESTING_NOTIFICATIONS = (FILE_NOTIFY_CHANGE_FILE_NAME |
                              FILE_NOTIFY_CHANGE_DIR_NAME |
                              FILE_NOTIFY_CHANGE_ATTRIBUTES |
                              FILE_NOTIFY_CHANGE_SIZE |
                              FILE_NOTIFY_CHANGE_LAST_WRITE |
                              FILE_NOTIFY_CHANGE_CREATION |
                              FILE_NOTIFY_CHANGE_SECURITY)


class Win32FileWatcher(object):
  """Monitors a directory tree for changes using inotify."""

  # TODO: evaluate whether we can directly support multiple directories.
  SUPPORTS_MULTIPLE_DIRECTORIES = False

  def __init__(self, directory):
    """Initializer for InotifyFileWatcher.

    Args:
      directory: A string representing the path to a directory that should
          be monitored for changes i.e. files and directories added, renamed,
          deleted or changed.
    """
    self._directory = os.path.abspath(directory)
    self._find_change_handle = None

  def start(self):
    """Start watching the directory for changes."""
    self._find_change_handle = (
        ctypes.windll.kernel32.FindFirstChangeNotificationA(
            self._directory,
            True,  # Recursive.
            _INTERESTING_NOTIFICATIONS))
    if self._find_change_handle == INVALID_HANDLE_VALUE:
      raise ctypes.WinError()

    if not ctypes.windll.kernel32.FindNextChangeNotification(
        self._find_change_handle):
      raise ctypes.WinError()

  def quit(self):
    """Stop watching the directory for changes."""
    ctypes.windll.kernel32.FindCloseChangeNotification(self._find_change_handle)

  def has_changes(self):
    """Returns True if the watched directory has changed since the last call.

    start() must be called before this method.

    Returns:
      Returns True if the watched directory has changed since the last call to
      has_changes or, if has_changes has never been called, since start was
      called.
    """
    found_change = False
    # Loop until no new changes are found. This prevents future calls to
    # has_changes() from returning True for changes that happened before this
    # call was made.
    while True:
      wait_result = ctypes.windll.kernel32.WaitForSingleObject(
          self._find_change_handle, 0)
      if wait_result == WAIT_OBJECT_0:
        if not ctypes.windll.kernel32.FindNextChangeNotification(
            self._find_change_handle):
          raise ctypes.WinError()
        found_change = True
        continue
      elif wait_result == WAIT_TIMEOUT:
        return found_change
      elif wait_result == WAIT_FAILED:
        raise ctypes.WinError()
      else:
        assert 'Unexpected result for WaitForSingleObject: %r' % wait_result
