
"""
This file contains the base MEOW monitor defintion. This should be inherited 
from for all monitor instances.

Author(s): David Marchant
"""

from copy import deepcopy
from threading import Lock
from typing import Union, Dict, List

from .base_pattern import BasePattern
from .base_recipe import BaseRecipe
from .rule import Rule
from .vars import VALID_CHANNELS, \
    VALID_MONITOR_NAME_CHARS, get_drt_imp_msg 
from ..functionality.validation import check_implementation, \
    valid_string, check_type, check_types, valid_dict_multiple_types
from ..functionality.meow import create_rules, create_rule
from ..functionality.naming import generate_monitor_id


class BaseMonitor:
    # An identifier for a monitor within the runner. Can be manually set in 
    # the constructor, or autogenerated if no name provided.
    name:str
    # A collection of patterns
    _patterns: Dict[str, BasePattern]
    # A collection of recipes
    _recipes: Dict[str, BaseRecipe]
    # A collection of rules derived from _patterns and _recipes
    _rules: Dict[str, Rule]
    # A channel for sending messages to the runner event queue. Note that this 
    # is not initialised within the constructor, but within the runner when the
    # monitor is passed to it unless the monitor is running independently of a
    # runner.
    to_runner_event: VALID_CHANNELS
    #A lock to solve race conditions on '_patterns'
    _patterns_lock:Lock
    #A lock to solve race conditions on '_recipes'
    _recipes_lock:Lock
    #A lock to solve race conditions on '_rules'
    _rules_lock:Lock
    def __init__(self, patterns:Dict[str,BasePattern], 
            recipes:Dict[str,BaseRecipe], name:str="")->None:
        """BaseMonitor Constructor. This will check that any class inheriting 
        from it implements its validation functions. It will then call these on
        the input parameters."""
        check_implementation(type(self).start, BaseMonitor)
        check_implementation(type(self).stop, BaseMonitor)
        check_implementation(type(self)._get_valid_pattern_types, BaseMonitor)
        self._is_valid_patterns(patterns)
        check_implementation(type(self)._get_valid_recipe_types, BaseMonitor)
        self._is_valid_recipes(recipes)
        # Ensure that patterns and recipes cannot be trivially modified from 
        # outside the monitor, as this will cause internal consistency issues
        self._patterns = deepcopy(patterns)
        self._recipes = deepcopy(recipes)
        self._rules = create_rules(patterns, recipes)
        if not name:
            name = generate_monitor_id()
        self._is_valid_name(name)
        self.name = name
        self._patterns_lock = Lock()
        self._recipes_lock = Lock()
        self._rules_lock = Lock()
        
    def __new__(cls, *args, **kwargs):
        """A check that this base class is not instantiated itself, only 
        inherited from"""
        if cls is BaseMonitor:
            msg = get_drt_imp_msg(BaseMonitor)
            raise TypeError(msg)
        return object.__new__(cls)

    def _is_valid_name(self, name:str)->None:
        """Validation check for 'name' variable from main constructor. Is 
        automatically called during initialisation. This does not need to be 
        overridden by child classes."""
        valid_string(name, VALID_MONITOR_NAME_CHARS)

    def _is_valid_patterns(self, patterns:Dict[str,BasePattern])->None:
        """Validation check for 'patterns' variable from main constructor."""
        valid_dict_multiple_types(
            patterns, 
            str, 
            self._get_valid_pattern_types(), 
            min_length=0, 
            strict=False
        )

    def _get_valid_pattern_types(self)->List[type]:
        """Validation check used throughout monitor to check that only 
        compatible patterns are used. Must be implmented by any child class."""
        raise NotImplementedError        

    def _is_valid_recipes(self, recipes:Dict[str,BaseRecipe])->None:
        """Validation check for 'recipes' variable from main constructor."""
        valid_dict_multiple_types(
            recipes, 
            str, 
            self._get_valid_recipe_types(), 
            min_length=0, 
            strict=False
        )

    def _get_valid_recipe_types(self)->List[type]:
        """Validation check used throughout monitor to check that only 
        compatible recipes are used. Must be implmented by any child class."""        
        raise NotImplementedError        

    def _identify_new_rules(self, new_pattern:BasePattern=None, 
            new_recipe:BaseRecipe=None)->None:
        """Function to determine if a new rule can be created given a new 
        pattern or recipe, in light of other existing patterns or recipes in 
        the monitor."""

        if new_pattern:
            self._patterns_lock.acquire()
            self._recipes_lock.acquire()
            try:
                # Check in case pattern has been deleted since function called
                if new_pattern.name not in self._patterns:
                    self._patterns_lock.release()
                    self._recipes_lock.release()
                    return
                # If pattern specifies recipe that already exists, make a rule
                if new_pattern.recipe in self._recipes:
                    self._create_new_rule(
                        new_pattern,
                        self._recipes[new_pattern.recipe],
                    )
            except Exception as e:
                self._patterns_lock.release()
                self._recipes_lock.release()
                raise e
            self._patterns_lock.release()
            self._recipes_lock.release()

        if new_recipe:
            self._patterns_lock.acquire()
            self._recipes_lock.acquire()
            try:
                # Check in case recipe has been deleted since function called
                if new_recipe.name not in self._recipes:
                    self._patterns_lock.release()
                    self._recipes_lock.release()
                    return
                # If recipe is specified by existing pattern, make a rule
                for pattern in self._patterns.values():
                    if pattern.recipe == new_recipe.name:
                        self._create_new_rule(
                            pattern,
                            new_recipe,
                        )
            except Exception as e:
                self._patterns_lock.release()
                self._recipes_lock.release()
                raise e
            self._patterns_lock.release()
            self._recipes_lock.release()

    def _identify_lost_rules(self, lost_pattern:str=None, 
            lost_recipe:str=None)->None:
        """Function to remove rules that should be deleted in response to a 
        pattern or recipe having been deleted."""
        to_delete = []
        self._rules_lock.acquire()
        try:
            # Identify any offending rules
            for name, rule in self._rules.items():
                if lost_pattern and rule.pattern.name == lost_pattern:
                    to_delete.append(name)
                if lost_recipe and rule.recipe.name == lost_recipe:
                    to_delete.append(name)
            # Now delete them
            for delete in to_delete:
                if delete in self._rules.keys():
                    self._rules.pop(delete)
        except Exception as e:
            self._rules_lock.release()
            raise e
        self._rules_lock.release()

    def _create_new_rule(self, pattern:BasePattern, recipe:BaseRecipe)->None:
        """Function to create a new rule from a given pattern and recipe. This 
        will only be called to create rules at runtime, as rules are 
        automatically created at initialisation using the  same 'create_rule' 
        function called here."""
        rule = create_rule(pattern, recipe)
        self._rules_lock.acquire()
        try:
            if rule.name in self._rules:
                raise KeyError("Cannot create Rule with name of "
                    f"'{rule.name}' as already in use")
            self._rules[rule.name] = rule
        except Exception as e:
            self._rules_lock.release()
            raise e
        self._rules_lock.release()

        self._apply_retroactive_rule(rule)

    def _apply_retroactive_rule(self, rule:Rule)->None:
        """Function to determine if a rule should be applied to any existing 
        defintions, if possible. May be implemented by inherited classes."""
        pass

    def _apply_retroactive_rules(self)->None:
        """Function to determine if any rules should be applied to any existing 
        defintions, if possible. May be implemented by inherited classes."""
        pass

    def send_event_to_runner(self, msg):
        self.to_runner_event.send(msg)

    def start(self)->None:
        """Function to start the monitor as an ongoing process/thread. Must be 
        implemented by any child process. Depending on the nature of the 
        monitor, this may wish to directly call apply_retroactive_rules before 
        starting."""
        pass

    def stop(self)->None:
        """Function to stop the monitor as an ongoing process/thread. Must be 
        implemented by any child process"""
        pass

    def add_pattern(self, pattern:BasePattern)->None:
        """Function to add a pattern to the current definitions. Any rules 
        that can be possibly created from that pattern will be automatically 
        created."""
        check_types(
            pattern, 
            self._get_valid_pattern_types(),
             hint="add_pattern.pattern"
        )

        self._patterns_lock.acquire()
        try:
            if pattern.name in self._patterns:
                raise KeyError(f"An entry for Pattern '{pattern.name}' "
                    "already exists. Do you intend to update instead?")
            self._patterns[pattern.name] = pattern
        except Exception as e:
            self._patterns_lock.release()
            raise e            
        self._patterns_lock.release()

        self._identify_new_rules(new_pattern=pattern)

    def update_pattern(self, pattern:BasePattern)->None:
        """Function to update a pattern in the current definitions. Any rules 
        created from that pattern will be automatically updated."""
        check_types(
            pattern, 
            self._get_valid_pattern_types(),
             hint="update_pattern.pattern"
        )

        self.remove_pattern(pattern.name)
        self.add_pattern(pattern)

    def remove_pattern(self, pattern: Union[str,BasePattern])->None:
        """Function to remove a pattern from the current definitions. Any rules 
        that will be no longer valid will be automatically removed."""
        check_type(
            pattern, 
            str, 
            alt_types=[BasePattern], 
            hint="remove_pattern.pattern"
        )
        lookup_key = pattern
        if isinstance(lookup_key, BasePattern):
            lookup_key = pattern.name
        self._patterns_lock.acquire()
        try:
            if lookup_key not in self._patterns:
                raise KeyError(f"Cannot remote Pattern '{lookup_key}' as it "
                    "does not already exist")
            self._patterns.pop(lookup_key)
        except Exception as e:
            self._patterns_lock.release()
            raise e 
        self._patterns_lock.release()

        if isinstance(pattern, BasePattern):
            self._identify_lost_rules(lost_pattern=pattern.name)
        else:
            self._identify_lost_rules(lost_pattern=pattern)
        
    def get_patterns(self)->Dict[str,BasePattern]:
        """Function to get a dict of the currently defined patterns of the 
        monitor. Note that the result is deep-copied, and so can be manipulated
        without directly manipulating the internals of the monitor."""
        to_return = {}
        self._patterns_lock.acquire()
        try:
            to_return = deepcopy(self._patterns)
        except Exception as e:
            self._patterns_lock.release()
            raise e
        self._patterns_lock.release()
        return to_return

    def add_recipe(self, recipe: BaseRecipe)->None:
        """Function to add a recipe to the current definitions. Any rules 
        that can be possibly created from that recipe will be automatically 
        created."""
        check_type(recipe, BaseRecipe, hint="add_recipe.recipe")
        self._recipes_lock.acquire()
        try:
            if recipe.name in self._recipes:
                raise KeyError(f"An entry for Recipe '{recipe.name}' already "
                    "exists. Do you intend to update instead?")
            self._recipes[recipe.name] = recipe
        except Exception as e:
            self._recipes_lock.release()
            raise e
        self._recipes_lock.release()

        self._identify_new_rules(new_recipe=recipe)

    def update_recipe(self, recipe: BaseRecipe)->None:
        """Function to update a recipe in the current definitions. Any rules 
        created from that recipe will be automatically updated."""
        check_type(recipe, BaseRecipe, hint="update_recipe.recipe")
        self.remove_recipe(recipe.name)
        self.add_recipe(recipe)
    
    def remove_recipe(self, recipe:Union[str,BaseRecipe])->None:
        """Function to remove a recipe from the current definitions. Any rules 
        that will be no longer valid will be automatically removed."""
        check_type(
            recipe, 
            str, 
            alt_types=[BaseRecipe], 
            hint="remove_recipe.recipe"
        )
        lookup_key = recipe
        if isinstance(lookup_key, BaseRecipe):
            lookup_key = recipe.name
        self._recipes_lock.acquire()
        try:
            # Check that recipe has not already been deleted
            if lookup_key not in self._recipes:
                raise KeyError(f"Cannot remote Recipe '{lookup_key}' as it "
                    "does not already exist")
            self._recipes.pop(lookup_key)
        except Exception as e:
            self._recipes_lock.release()
            raise e
        self._recipes_lock.release()

        if isinstance(recipe, BaseRecipe):
            self._identify_lost_rules(lost_recipe=recipe.name)
        else:
            self._identify_lost_rules(lost_recipe=recipe)

    def get_recipes(self)->Dict[str,BaseRecipe]:
        """Function to get a dict of the currently defined recipes of the 
        monitor. Note that the result is deep-copied, and so can be manipulated
        without directly manipulating the internals of the monitor."""
        to_return = {}
        self._recipes_lock.acquire()
        try:
            to_return = deepcopy(self._recipes)
        except Exception as e:
            self._recipes_lock.release()
            raise e
        self._recipes_lock.release()
        return to_return
    
    def get_rules(self)->Dict[str,Rule]:
        """Function to get a dict of the currently defined rules of the 
        monitor. Note that the result is deep-copied, and so can be manipulated
        without directly manipulating the internals of the monitor."""
        to_return = {}
        self._rules_lock.acquire()
        try:
            to_return = deepcopy(self._rules)
        except Exception as e:
            self._rules_lock.release()
            raise e
        self._rules_lock.release()
        return to_return


