import os
import sys
import re
import handlers
import logging
import importlib

VALID_MODULE_NAME = re.compile(r'^(\w+)\.py$', re.IGNORECASE)
PARENT_DIRNAME = os.path.basename(os.path.dirname(os.path.realpath(__file__)))
BASE_DIR = os.path.dirname(os.path.realpath(
    os.path.join(__file__, os.pardir)))

logger = logging.getLogger(__name__)
re_schedule_handler = re.compile('^ScheduleHandler_(\S+)$')
re_storage_handler = re.compile('^StorageHandler_(\S+)$')

# FIXME(mpavlase): Figure out nicer way to deal with paths
sys.path.append(BASE_DIR)

def get_local_path(path):
    return os.path.join(BASE_DIR, PARENT_DIRNAME, path)


class AutodiscoverHandlers(object):
    _discovered_handlers = None
    re_class_teplate = None

    def __init__(self, re_class_template):
        self.re_class_teplate = re_class_template
        self._discovered_handlers = dict()

    def get_handlers(self):
        return self._discovered_handlers

    @staticmethod
    def _load_module(name):
        importlib.import_module(name)
        return sys.modules[name]

    def _find_classes(self, module):
        ret = dict()

        for name in dir(module):
            obj = getattr(module, name)
            is_inst = isinstance(obj, type)

            if not is_inst:
                continue

            if obj == handlers.ScheduleHandlerBase:
                continue

            # TODO(mpavlase): figure out more reliable way to test subclasses
            #is_subclass = issubclass(obj, handlers.ScheduleHandlerBase)
            #if not is_subclass:
            #    continue

            key = re.findall(self.re_class_teplate, obj.__name__)
            if not key:
                continue

            key = key[0]
            val = dict()
            val['class'] = obj
            try:
                val['provide_export'] = obj.provide_export
            except AttributeError:
                # Not all handlers has this attr
                # TODO(mpavlase): consider this behavior implement in subclass
                pass

            ret[key] = val
            logger.debug('Discovered new handler: {} from {}'.format(key, module))
        return ret

    def _discover_path(self, filename, parent_module):
        # valid Python identifiers only
        if not VALID_MODULE_NAME.match(filename):
            return

        name = VALID_MODULE_NAME.sub('\\1', filename)
        name = '.'.join([parent_module, name])
        loaded_module = self._load_module(name)
        classes = self._find_classes(loaded_module)

        for k in classes.keys():
            if k in self._discovered_handlers.keys():
                cls_existing = self._discovered_handlers[k]
                cls_new = classes[k]
                msg = ('Found handler with name \'{}\', that already exists '
                       'and would be overridden (existing: {}, '
                       'new: {})').format(k, cls_existing, cls_new)
                logger.info(msg)

        self._discovered_handlers.update(classes)

    def discover(self, pypath):
        try:
            loaded_module = self._load_module(pypath)
        except ImportError as e:
            logger.warn('Skipping path "{}", couldn\'t load'
                        'it: {} )'.format(pypath, e))
            return self._discovered_handlers

        module_path = os.path.dirname(loaded_module.__file__)
        files = os.listdir(module_path)

        for filename in files:
            self._discover_path(filename, pypath)

        return self._discovered_handlers


class LazyDictDiscovery(dict):
    autodiscovery = None
    discovery_run_flag = True

    def __init__(self, *args, **kwargs):
        self.autodiscovery = AutodiscoverHandlers(kwargs.pop('cls_template'))
        self.update(dict(*args, **kwargs))  # use the free update to set keys
        super(LazyDictDiscovery, self).__init__(*args, **kwargs)

    def __getitem__(self, item):
        if not self.items() or self.discovery_run_flag:
            self.run_discovery()

        return super(LazyDictDiscovery, self).__getitem__(item)

    def keys(self):
        if not self.items() or self.discovery_run_flag:
            self.run_discovery()
        return super(LazyDictDiscovery, self).keys()

    def values(self):
        if not self.items() or self.discovery_run_flag:
            self.run_discovery()
        return super(LazyDictDiscovery, self).values()

    def _post_discovery_hook(self):
        """
        Override this method, if you need to do something after discovery has
        been processed.
        """
        pass

    def add_discover_path(self, path):
        search_paths.append(path)
        self.discovery_run_flag = True

    def run_discovery(self):
        ret = dict()

        for pypath in search_paths:
            logger.debug('Searching for handlers in: {}'.format(pypath))
            ret = self.autodiscovery.discover(pypath)

        # override all existing keys/values
        self.clear()
        for key, value in ret.items():
            self[key] = value

        self._post_discovery_hook()
        self.discovery_run_flag = False
        return ret


class ScheduleHandlerDiscovery(LazyDictDiscovery):
    _provided_exports = []

    @property
    def provided_exports(self):
        if not self._provided_exports or self.discovery_run_flag:
            self.run_discovery()
        return self._provided_exports

    def _post_discovery_hook(self):
        for handler_name, handler in self.items():
            if handler['provide_export']:
                self._provided_exports.append(handler_name)

        self._provided_exports = sorted(self._provided_exports)


class StorageHandlerDiscovery(LazyDictDiscovery):
    pass

schedule_handlers = ScheduleHandlerDiscovery(cls_template=re_schedule_handler)
storage_handlers = StorageHandlerDiscovery(cls_template=re_storage_handler)

search_paths = ['schedules_tools.handlers',
                'schedules_tools.storage']

@@ -3588,32 +3588,32 @@
     
-discovery_run_flag = Tru
+_last_search_paths = Non
 e%0A%0A 
@@ -3911,68 +3911,8 @@
 m):%0A
-        if not self.items() or self.discovery_run_flag:%0A    
     
@@ -3936,17 +3936,16 @@
 overy()%0A
-%0A
         
@@ -4025,68 +4025,8 @@
 f):%0A
-        if not self.items() or self.discovery_run_flag:%0A    
     
@@ -4130,68 +4130,8 @@
 f):%0A
-        if not self.items() or self.discovery_run_flag:%0A    
     
@@ -4399,72 +4399,73 @@
 def 
-add_discover_path(self, path):%0A        search_paths.append(path)
+force_run_discovery(self):%0A        self._last_search_paths = None
 %0A   
@@ -4466,32 +4466,36 @@
 ne%0A        self.
+run_
 discovery_run_fl
@@ -4491,24 +4491,10 @@
 very
-_run_flag = True
+()
 %0A%0A  
@@ -4512,32 +4512,158 @@
 iscovery(self):%0A
+        # run only when search_paths has been changed%0A        if self._last_search_paths == search_paths:%0A            return%0A%0A
         ret = di
@@ -5020,53 +5020,41 @@
 elf.
-discovery_run_flag = False%0A        return ret
+_last_search_paths = search_paths
 %0A%0A%0Ac

improve determining if new discovery is needed