#!/usr/bin/env python
# -*- coding: utf-8 -*-

import __builtin__
import os
import re
import sys
import threading

# Install the new import function:
def custom_import_install(web2py_path):
        global _web2py_importer
        global _web2py_path
        if _web2py_importer:
            return  # Already installed
        _web2py_path = web2py_path
        _web2py_importer = _Web2pyImporter(web2py_path)
        __builtin__.__import__ = _web2py_importer

def is_tracking_changes():
    """
    @return: True: neo_importer is tracking changes made to Python source
    files. False: neo_import does not reload Python modules.
    """

    global _is_tracking_changes
    return _is_tracking_changes

def track_changes(track=True):
    """
    Tell neo_importer to start/stop tracking changes made to Python modules.
    @param track: True: Start tracking changes. False: Stop tracking changes.
    """

    global _is_tracking_changes
    global _web2py_importer
    global _web2py_date_tracker_importer
    assert track is True or track is False, "Boolean expected."
    if track == _is_tracking_changes:
        return
    if track:
        if not _web2py_date_tracker_importer:
            _web2py_date_tracker_importer = \
              _Web2pyDateTrackerImporter(_web2py_path)
        __builtin__.__import__ = _web2py_date_tracker_importer
    else:
        __builtin__.__import__ = _web2py_importer
    _is_tracking_changes = track

_STANDARD_PYTHON_IMPORTER = __builtin__.__import__ # Keep standard importer
_web2py_importer = None # The standard web2py importer
_web2py_date_tracker_importer = None # The web2py importer with date tracking
_web2py_path = None # Absolute path of the web2py directory

_is_tracking_changes = False # The tracking mode

class _BaseImporter(object):
    """
    The base importer. Dispatch the import the call to the standard Python
    importer.
    """

    def begin(self):
        """
        Many imports can be made for a single import statement. This method
        help the management of this aspect.
        """

    def __call__(self, name, globals={}, locals={}, fromlist=[], level=-1):
        """
        The import method itself.
        """
        return _STANDARD_PYTHON_IMPORTER(name, globals, locals, fromlist,
                                         level)

    def end(self):
        """
        Needed for clean up.
        """


class _DateTrackerImporter(_BaseImporter):
    """
    An importer tracking the date of the module files and reloading them when
    they have changed.
    """

    _PACKAGE_PATH_SUFFIX = os.path.sep+"__init__.py"

    def __init__(self):
        super(_DateTrackerImporter, self).__init__()
        self._import_dates = {} # Import dates of the files of the modules
        # Avoid reloading cause by file modifications of reload:
        self._tl = threading.local()
        self._tl._modules_loaded = None

    def begin(self):
        self._tl._modules_loaded = set()

    def __call__(self, name, globals={}, locals={}, fromlist=[], level=-1):
        """
        The import method itself.
        """

        call_begin_end = self._tl._modules_loaded == None
        if call_begin_end:
            self.begin()

	try:
            self._tl.globals = globals
            self._tl.locals = locals
            self._tl.level = level

            # Check the date and reload if needed:
            self._update_dates(name, fromlist)

            # Try to load the module and update the dates if it works:
            result = super(_DateTrackerImporter, self) \
              .__call__(name, globals, locals, fromlist, level)
            # Module maybe loaded for the 1st time so we need to set the date
            self._update_dates(name, fromlist)
            return result
        except Exception, e:
            raise e  # Don't hide something that went wrong
        finally:
            if call_begin_end:
                self.end()

    def _update_dates(self, name, fromlist):
        """
        Update all the dates associated to the statement import. A single
        import statement may import many modules.
        """

        self._reload_check(name)
        if fromlist:
            for fromlist_name in fromlist:
                self._reload_check("%s.%s" % (name, fromlist_name))

    def _reload_check(self, name):
        """
        Update the date associated to the module and reload the module if
        the file has changed.
        """

        module = sys.modules.get(name)
        file = self._get_module_file(module)
        if file:
            date = self._import_dates.get(file)
            new_date = None
            reload_mod = False
            mod_to_pack = False # Module turning into a package? (special case)
            try:
                new_date = os.path.getmtime(file)
            except:
                self._import_dates.pop(file, None)  # Clean up
                # Handle module changing in package and
                #package changing in module:
                if file.endswith(".py"):
                    # Get path without file ext:
                    file = os.path.splitext(file)[0]
                    reload_mod = os.path.isdir(file) \
                      and os.path.isfile(file+self._PACKAGE_PATH_SUFFIX)
                    mod_to_pack = reload_mod
                else: # Package turning into module?
                    file += ".py"
                    reload_mod = os.path.isfile(file)
                if reload_mod:
                    new_date = os.path.getmtime(file) # Refresh file date
            if reload_mod or not date or new_date > date:
                self._import_dates[file] = new_date
            if reload_mod or (date and new_date > date):
                if module not in self._tl._modules_loaded:
                    if mod_to_pack:
                        # Module turning into a package:
                        mod_name = module.__name__
                        del sys.modules[mod_name] # Delete the module
                        # Reload the module:
                        super(_DateTrackerImporter, self).__call__ \
                          (mod_name, self._tl.globals, self._tl.locals, [],
                           self._tl.level)
                    else:
                        reload(module)
                        self._tl._modules_loaded.add(module)

    def end(self):
        self._tl._modules_loaded = None

    @classmethod
    def _get_module_file(cls, module):
        """
        Get the absolute path file associated to the module or None.
        """

        file = getattr(module, "__file__", None)
        if file:
            # Make path absolute if not:
            #file = os.path.join(cls.web2py_path, file)

            file = os.path.splitext(file)[0]+".py" # Change .pyc for .py
            if file.endswith(cls._PACKAGE_PATH_SUFFIX):
                file = os.path.dirname(file)  # Track dir for packages
        return file

class _Web2pyImporter(_BaseImporter):
    """
    The standard web2py importer. Like the standard Python importer but it
    tries to transform import statements as something like
    "import applications.app_name.modules.x". If the import failed, fall back
    on _BaseImporter.
    """

    _RE_ESCAPED_PATH_SEP = re.escape(os.path.sep)  # os.path.sep escaped for re

    def __init__(self, web2py_path):
        """
        @param web2py_path: The absolute path of the web2py installation.
        """

        global DEBUG
        super(_Web2pyImporter, self).__init__()
        self.web2py_path =  web2py_path
        self.__web2py_path_os_path_sep = self.web2py_path+os.path.sep
        self.__web2py_path_os_path_sep_len = len(self.__web2py_path_os_path_sep)
        self.__RE_APP_DIR = re.compile(
          self._RE_ESCAPED_PATH_SEP.join( \
          ( \
            #"^" + re.escape(web2py_path),   # Not working with Python 2.5
            "^(" + "applications",
            "[^",
            "]+)",
            "",
          ) ))

    def _matchAppDir(self, file_path):
        """
        Does the file in a directory inside the "applications" directory?
        """

        if file_path.startswith(self.__web2py_path_os_path_sep):
            file_path = file_path[self.__web2py_path_os_path_sep_len:]
            return self.__RE_APP_DIR.match(file_path)
        return False

    def __call__(self, name, globals={}, locals={}, fromlist=[], level=-1):
        """
        The import method itself.
        """

        self.begin()
        #try:
        # if not relative and not from applications:
        if not name.startswith(".") and level <= 0 \
                    and not name.startswith("applications.") \
                    and isinstance(globals, dict):
            # Get the name of the file do the import
            caller_file_name = os.path.join(self.web2py_path, \
                                            globals.get("__file__", ""))
            # Is the path in an application directory?
            match_app_dir = self._matchAppDir(caller_file_name)
            if match_app_dir:
                try:
                    # Get the prefix to add for the import
                    # (like applications.app_name.modules):
                    modules_prefix = \
                        ".".join((match_app_dir.group(1). \
                        replace(os.path.sep, "."), "modules"))
                    if not fromlist:
                        # import like "import x" or "import x.y"
                        return self.__import__dot(modules_prefix, name,
                            globals, locals, fromlist, level)
                    else:
                        # import like "from x import a, b, ..."
                        return super(_Web2pyImporter, self) \
                            .__call__(modules_prefix+"."+name,
                                    globals, locals, fromlist, level)
                except ImportError:
                    pass
        return super(_Web2pyImporter, self).__call__(name, globals, locals,
                                                    fromlist, level)
        #except Exception, e:
        #    raise e  # Don't hide something that went wrong
        #finally:
        self.end()

    def __import__dot(self, prefix, name, globals, locals, fromlist,
                      level):
        """
        Here we will import x.y.z as many imports like:
        from applications.app_name.modules import x
        from applications.app_name.modules.x import y
        from applications.app_name.modules.x.y import z.
        x will be the module returned.
        """

        result = None
        for name in name.split("."):
            new_mod = super(_Web2pyImporter, self).__call__(prefix, globals,
                                                        locals, [name], level)
            try:
                result = result or new_mod.__dict__[name]
            except KeyError:
                raise ImportError()
            prefix += "." + name
        return result

class _Web2pyDateTrackerImporter(_Web2pyImporter, _DateTrackerImporter):
    """
    Like _Web2pyImporter but using a _DateTrackerImporter.
    """

