import collections
import json
import numbers
import threading
import os

from . import exceptions

##############################################################################################################
#
# NOTE !!!
#
# THIS FILE MAY BE AUTOGENERATED.
# ONLY MODIFY THE ORIGINAL FILE IN THE /sharedData/ FOLDER!
# OTHER INSTANCES OF THIS FILE ARE COPIES!
#
# NOTE !!!
#
##############################################################################################################


_TRUTH_FILE_LOCATION = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'TRUTH.txt')
_truth = None
def get_shared_truth():
    """
    uses TRUTH.txt to return a dictionary of constants that are shared across programs.
    """
    global _truth
    path = _TRUTH_FILE_LOCATION
    if _truth is None:
        with open(path, 'r') as f:
            _truth = json.load(f)
    return _truth


class Identifier:
    """
    a token by which objects of different types can be compared.
    This has a serverside SQL counterpart called IdentifierModel.
    All objects that are read from the input have an identifier.
    All objects generated during the output have a preliminary identifier, because the output will be interpreted by the Execution Environment.
    (preliminary identifiers can be used like any other).
    An identifier may optionally also have a name.
    Identifiers provided by the server will have this field set if the Identifier refers to an object with a name.
    Some objects can actually be uniquely identified by their name. In those cases, if both an ID and a name are given, the ID takes precedence.
    """
    def __init__(self, model_id, type, preliminary=False, name=None):
        self.id = model_id
        self.type = type
        self.preliminary = preliminary
        self.name = name
    def __str__(self):
        return "%s Identifier %s%s" % (self.type, self.id, " (preliminary)" if self.preliminary else "")
    def __hash__(self):
        """
        override the default hashing, to make equality comparisons possible
        """
        res = hash(self.type)
        res *= 17
        res += hash(self.id)
        res *= 17
        res += hash(self.preliminary)
        return res
    def __eq__(self, other):
        """
        Override the default Equals behavior
        """
        return (self.type == other.type) and (self.id == other.id) and (self.preliminary == other.preliminary)
    def __ne__(self, other):
        """
        Define a non-equality test
        """
        return not self.__eq__(other)
    def to_json(self):
        """
        gives a JSON dictionary representation of this identifier.
        Counterpart to parse_identifier().
        """
        res = {
            'type' : self.type,
            'id' : self.id,
        }
        if self.preliminary:
            res['preliminary'] = True
        if self.name:
            res['name'] = self.name
        return res
    def get_program_name_and_version_separately(self):
        """
        if this Identifier refers to a program and has a name, splits the name into the base name and the version and returns a tuple of both.
        """
        if self.type != 'program':
            raise ValueError("this Identifier does not refer to a program.")
        if self.name is None:
            raise ValueError("this program Identifier does not have its name set.")
        if '#' not in self.name:
            raise ValueError("no version is given for this Identifier. This should not be possible.")
        l = self.name.split('#')
        name = l[0]
        version = int(l[1])
        return name, version


def parse_identifier(dictionary):
    """
    creates an Identifier from a JSON dictionary structure
    Counterpart to Identifier.to_json().
    Raises an InvalidParamsException if the format is incorrect.
    """
    if not isinstance(dictionary, dict):
        raise exceptions.InvalidParamsException("couldn't parse an Identifier. The object was not a dictionary describing the identifier.")
    id = dictionary.get('id', None)
    type = dictionary.get('type', None)
    event_types = [a[0] for a in get_shared_truth()['valid_identifier_types']]
    if type not in event_types:
        raise exceptions.InvalidParamsException("couldn't parse an Identifier. There was no string field 'type' with a value of one of (%s)." % (', '.join(event_types),))
    preliminary = dictionary.get('preliminary', False)
    preliminary = True if preliminary else False
    name = dictionary.get('name', None)
    if id is None and name is None:
        raise exceptions.InvalidParamsException("couldn't parse an Identifier. Either an ID or a name must be given.")
    if id is not None and not isinstance(id, int):
        raise exceptions.InvalidParamsException("couldn't parse an Identifier. The field 'id' must be either an integer or null.")
    if name is not None and not isinstance(name, str):
        raise exceptions.InvalidParamsException("couldn't parse an Identifier. The field 'name' must be either a string or null.")
    return Identifier(id, type, preliminary=preliminary, name=name)


_preliminary_identifier_counter_lock = threading.RLock()
_preliminary_identifier_counter = 0
def create_preliminary_identifier(type, name=None):
    """
    creates a preliminary Identifier, for use by output objects.
    """
    with _preliminary_identifier_counter_lock:
        global _preliminary_identifier_counter
        c = _preliminary_identifier_counter
        _preliminary_identifier_counter += 1
    res = Identifier(c, type, preliminary=True, name=name)
    return res


class Program:
    """
    represents a Program on the server.
    This object is only created from the corresponding serverside object, which is why no type checking or parameter verification is done here.
    minor TODO:
    it's a bit inconsistent: Program has its name and version stored separately as fields, while Rule only has it stored indirectly through Identifier.
    decide on one of the two ways and use it for both.
    Probably the way Rule does it is better: using the identifier for this avoids redundancy
    """
    def __init__(self, identifier, creator_id, name, version, description, rating_numerator, rating_count):
        self.identifier = identifier
        self.creator_id = creator_id
        self.name = name
        self.version = version
        self.description = description
        self.rating_numerator = rating_numerator
        self.rating_count = rating_count
    def to_json(self):
        """
        creates a JSON dictionary structure form this object.
        Counterpart to parse_program().
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'creator_id' : self.creator_id,
            'name' : self.name,
            'version' : self.version,
            'description' : self.description,
            'rating_numerator' : self.rating_numerator,
            'rating_count' : self.rating_count,
        }
        return res


def parse_program(dictionary):
    """
    creates an Program from a JSON dictionary structure
    Counterpart to Program.to_json().
    """
    identifier = parse_identifier(dictionary['identifier'])
    creator_id = dictionary['creator_id']
    name = dictionary['name']
    version = dictionary['version']
    description = dictionary['description']
    rating_numerator = dictionary['rating_numerator']
    rating_count = dictionary['rating_count']
    res = Program(identifier, creator_id, name, version, description, rating_numerator, rating_count)
    return res


class Symbol:
    """
    represents a SymbolModel on the server.
    Symbols have a name and a description, and exist for communication purposes.
    """
    def __init__(self, identifier, name, description, private=False, creator_id=None):
        self.identifier = identifier
        self.name = name
        self.description = description
        self.private = private
        self.creator_id = creator_id
    def to_json(self):
        """
        creates a JSON dictionary structure form this object.
        Counterpart to parse_program().
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'name' : self.name,
            'description' : self.description,
            'private' : self.private,
            'creator_id' : self.creator_id,
        }
        return res


def parse_symbol(dictionary):
    """
    creates a Symbol from a JSON dictionary structure
    Counterpart to Symbol.to_json().
    """
    identifier = parse_identifier(dictionary['identifier'])
    name = dictionary['name']
    description = dictionary['description']
    private = dictionary['private']
    creator_id = dictionary['creator_id']
    res = Symbol(identifier, name, description, private=private, creator_id=creator_id)
    return res


class Rule:
    """
    represents a RuleModel on the server.
    """
    def __init__(self, identifier, creator_id, description, dependencies, threshold, trigger_rule, actions, existing_arguments, rating_numerator, rating_count):
        self.identifier = identifier
        self.creator_id = creator_id
        self.description = description
        self.dependencies = dependencies
        self.threshold = threshold
        self.trigger_rule = trigger_rule
        self.actions = actions
        self.existing_arguments = existing_arguments
        self.rating_numerator = rating_numerator
        self.rating_count = rating_count
    def to_json(self):
        """
        returns a JSON representation of the Rule object.
        This is a counterpart to parse_exec_rule(), but not exactly, because parse_exec_rule() creation will fill in missing default values.
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'creator_id' : self.creator_id,
            'description' : self.description,
            'dependencies' : self.dependencies,
            'threshold' : self.threshold,
            'trigger_rule' : self.trigger_rule,
            'actions' : self.actions,
            'existing_arguments' : self.existing_arguments,
            'rating_numerator' : self.rating_numerator,
            'rating_count' : self.rating_count,
        }
        return res


def parse_rule(dictionary):
    """
    creates an Rule from a JSON dictionary structure.
    Counterpart to Rule.to_json().
    """
    # the identifier
    identifier = parse_identifier(dictionary['identifier'])
    creator_id = dictionary['creator_id']
    description = dictionary['description']
    dependencies = dictionary['dependencies']
    threshold = dictionary['threshold']
    trigger_rule = dictionary['trigger_rule']
    actions = dictionary['actions']
    existing_arguments = dictionary['existing_arguments']
    rating_numerator = dictionary['rating_numerator']
    rating_count = dictionary['rating_count']
    res = Rule(identifier, creator_id, description, dependencies, threshold, trigger_rule, actions, existing_arguments, rating_numerator, rating_count)
    return res


class FileObject:
    """
    represents a file that was given as an input argument, along with information about it.
    """
    def __init__(self, identifier, file_name, creation_step, creation_index, trigger=None, creator_id=None):
        self.identifier = identifier
        self.file_name = file_name
        self.creation_step = creation_step # set by the server; values provided by the user are ignored
        self.creation_index = creation_index # set by the server; values provided by the user are ignored
        self.trigger = trigger # set by the server; values provided by the user are ignored
        self.creator_id = creator_id # set by the server; values provided by the user are ignored
    def __str__(self):
        return self.identifier
    def _change_file_name_for_use_as_input(self, arg_name):
        """
        replaces the name of the file with a name that is used for input files.
        This method should be used by the ExecutionEnvironment after copying a file to use it as input for another program execution.
        """
        self.file_name = "arg_%s" % arg_name
    def to_json(self):
        """
        gives a JSON dictionary representation of this FileObject that can be parsed as a new FileObject.
        this is a counterpart to parse_file_object().
        """
        res = {}
        res['file'] =  self.file_name
        res['identifier'] = self.identifier.to_json()
        res['creation_step'] = self.creation_step
        res['creation_index'] = self.creation_index
        res['trigger'] = self.trigger
        res['creator_id'] = self.creator_id
        return res


def parse_file_object(dictionary):
    """
    creates a FileObject from a JSON dictionary structure
    Counterpart to FileObject.to_json()
    """
    identifier = parse_identifier(dictionary['identifier'])
    file_name = dictionary['file']
    creation_step = dictionary['creation_step']
    creation_index = dictionary['creation_index']
    trigger = dictionary['trigger']
    creator_id = dictionary['creator_id']
    res = FileObject(identifier, file_name, creation_step, creation_index, trigger=trigger, creator_id=creator_id)
    return res


class Tag:
    """
    represents a TagModel on the server.
    A Tag consists of a Symbol, defined here as a string, and a number of identifiers.
    It also has an identifier of its own.
    Optionally, it may also have a comment and a weight.
    NOTE:
    when defining a Tag, it is ok to specify only the symbol_name. The symbol_identifier will then be set by the server later.
    This does however mean that the created Tag can't be safely converted back from json() until it has been cleaned up by the server.
    """
    def __init__(self, own_identifier, arguments, symbol_name=None, symbol_identifier=None, comment=None, weight=None, trigger=None, creator_id=None):
        self.identifier = own_identifier
        self.symbol_name = symbol_name
        self.symbol_identifier = symbol_identifier
        self.argument_identifiers = [a if isinstance(a, Identifier) else a.identifier for a in arguments]
        self.comment = comment
        self.weight = weight
        self.trigger = trigger # set by the server; values provided by the user are ignored
        self.creator_id = creator_id # set by the server; values provided by the user are ignored
    def __str__(self):
        return "Tag %s: symbol=%s, arguments=(%s)" % (self.identifier, self.symbol_identifier.name, ', '.join(['%s' % a for a in self.argument_identifiers]))
    def to_json(self):
        """
        gives a JSON dictionary representation of this Tag.
        Counterpart to parse_tag().
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'symbol_name' : self.symbol_name,
            'symbol_identifier' : None if self.symbol_identifier is None else self.symbol_identifier.to_json(),
            'argument_identifiers' : [a.to_json() for a in self.argument_identifiers],
            'comment' : self.comment,
            'weight' : self.weight,
            'trigger' : self.trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_tag(dictionary):
    """
    creates a Tag from a JSON dictionary structure
    Counterpart to Tag.to_json().
    """
    identifier = parse_identifier(dictionary['identifier'])
    symbol_name = dictionary['symbol_name']
    symbol_identifier = parse_identifier(dictionary['symbol_identifier'])
    argument_identifiers = [parse_identifier(a) for a in dictionary['argument_identifiers']]
    comment = dictionary['comment']
    weight = dictionary['weight']
    trigger = dictionary['trigger']
    creator_id = dictionary['creator_id']
    return Tag(identifier, argument_identifiers, symbol_name=symbol_name, symbol_identifier=symbol_identifier,
        comment=comment, weight=weight, trigger=trigger, creator_id=creator_id)


class TagRequest():
    """
    a class that uses method-chaining to create a Tag.
    """
    def __init__(self):
        self.content = Tag(create_preliminary_identifier('tag'), [])
    def symbol(self, name_or_identifier):
        """
        the symbol can be specified either as a string, a Symbol object or an Identifier.
        """
        if isinstance(name_or_identifier, str):
            self.content.symbol_name = name_or_identifier
        elif isinstance(name_or_identifier, Symbol):
            self.content.symbol_name = name_or_identifier.name
        elif isinstance(name_or_identifier, Identifier) and name_or_identifier.type == 'symbol':
            self.content.symbol_name = name_or_identifier.name
        else:
            raise ValueError("the Symbol of the Tag must be specified either through a string, a Symbol object or an Identifier")
        return self
    def arguments(self, arguments):
        """
        the arguments of a Tag must be specified either as Identifiers or as objects with an Identifier as a field.
        """
        argument_identifiers = []
        for arg in arguments:
            if isinstance(arg, Identifier):
                argument_identifiers.append(arg)
            else:
                argument_identifiers.append(arg.identifier)
        self.content.argument_identifiers = argument_identifiers
        return self
    def comment(self, comment):
        if comment is not None and not isinstance(comment, str):
            raise ValueError("the comment must be either None or a String")
        self.content.comment = comment
        return self
    def weight(self, weight):
        if weight is not None and not isinstance(weight, numbers.Number):
            raise ValueError("the weight must be either None or a number")
        self.content.weight = weight
        return self
    def to_json(self):
        return self.content.to_json()


class Message:
    """
    represents a message that is displayed to the user. Can contain interactive features.
    """
    def __init__(self, identifier, message_components, only_for_developers, trigger=None, creator_id=None):
        self.identifier = identifier
        self.message_components = message_components
        self.only_for_developers = only_for_developers
        self.trigger = trigger # set by the server; values provided by the user are ignored
        self.creator_id = creator_id # set by the server; values provided by the user are ignored
    def to_json(self):
        """
        gives a JSON dictionary representation of this Message that can be parsed as a new Message.
        this is a counterpart to parse_message().
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'message_components' : self.message_components,
            'only_for_developers' : self.only_for_developers,
            'trigger' : self.trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_message(dictionary):
    """
    creates a Message from a JSON dictionary structure
    Counterpart to Message.to_json()
    """
    identifier = parse_identifier(dictionary['identifier'])
    message_components = dictionary['message_components']
    only_for_developers = dictionary['only_for_developers']
    trigger = dictionary['trigger']
    creator_id = dictionary['creator_id']
    res = Message(identifier, message_components, only_for_developers, trigger=trigger, creator_id=creator_id)
    return res


class DisplayMessageRequest(Message):
    """
    a class that uses method-chaining to create a message to the user in an interactive website.
    The message generated here will be put in a HTML form and displayed.
    """
    def __init__(self, identifier=None):
        super().__init__(create_preliminary_identifier('message') if identifier is None else identifier, [], False)
    def add_text(self, message_text):
        if not isinstance(message_text, str):
            raise ValueError("The message to display must be a string")
        component = {
            'type' : 'unformatted_text',
            'text' : message_text,
        }
        self.message_components.append(component)
        return self
    def add_plot(self, data, options=None):
        component = {
            'type' : 'plot',
            'data' : data,
            'options' : {} if options is None else options,
        }
        self.message_components.append(component)
        return self
    def add_request_for_rating(self, target, event=None):
        """
        add a request to rate a program or rule.
        Note:
        when adding a feedback request in this way, the event may be set to None.
        If it is, it defaults to the currently executed event when the server parses it.
        This is because the event should only be None for the global feedback_request of an object, which is only created alongside that object.
        """
        if not isinstance(target, Identifier):
            try:
                target = target.identifier
            except:
                raise ValueError("the target of a feedback_request must be given as an Identifier or as an object with an identifier field")
        if target is not None and not isinstance(target, Identifier):
            try:
                event = event.identifier
            except:
                raise ValueError("the event of a feedback_request must be given as an Identifier or as an object with an identifier field")
        component = {
            'type' : 'request_for_rating',
            'feedback_request' : {
                'feedback_type' : 'rating',
                'target_identifier' : target.to_json(),
                'event_identifier' : None if event is None else event.to_json(),
            },
        }
        self.message_components.append(component)
        return self
    def is_only_for_developers(self, val=True):
        self.only_for_developers = val
        return self


class Event:
    """
    represents an event in the Execution Environment.
    """
    def __init__(self, identifier, event_type, args, priority=False, triggering_step=None, trigger=None, creator_id=None):
        self.identifier = identifier
        self.event_type = event_type
        self.args = args
        self.priority = priority
        self.triggering_step = triggering_step # set by the server; values provided by the user are ignored when this gets parsed by the server
        self.trigger = trigger # set by the server; values provided by the user are ignored
        self.creator_id = creator_id # set by the server; values provided by the user are ignored
    def set_priority(self, priority=True):
        """
        mark the event as a priority.
        Priority events are executed before any of the other ones.
        """
        self.priority = priority
        return self
    def to_json(self):
        """
        gives a JSON dictionary representation of this Event.
        this is a counterpart to parse_event_request().
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'event_type' : self.event_type,
            'priority' : self.priority,
            'args' : self.args,
            'triggering_step' : self.triggering_step,
            'trigger' : self.trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_event(dictionary):
    """
    creates an Event from a JSON dictionary structure
    Counterpart to Event.to_json().
    """
    identifier = parse_identifier(dictionary['identifier'])
    event_type = dictionary['event_type']
    priority = dictionary['priority']
    args = dictionary['args']
    triggering_step = dictionary['triggering_step']
    trigger = dictionary['trigger']
    creator_id = dictionary['creator_id']
    res = Event(identifier, event_type, args, priority=priority, triggering_step=triggering_step, trigger=trigger, creator_id=creator_id)
    return res


class ProgramExecutionRequest(Event):
    """
    a class that uses method-chaining to create a request to execute a Program.
    """
    def __init__(self):
        super().__init__(create_preliminary_identifier('event'), 'execute_program', {'argument_dict' : {}})
    def program(self, program_identifier):
        error_message = """The program may be identified as an Identifier,
            as a String displaying the name of the program (in which case the latest version is picked),
            as a String of <name>#<version> (which identifies the version directly),
            or as an integer that is the program's ID (which is unambiguous and includes the version)"""
        if isinstance(program_identifier, Identifier):
            self.args['program_identifier'] = program_identifier
        elif isinstance(program_identifier, int):
            self.args['program_identifier'] = Identifier(program_identifier, 'program', preliminary=False, name=None)
        elif isinstance(program_identifier, str):
            self.args['program_identifier'] = Identifier(None, 'program', preliminary=False, name=program_identifier)
        else:
            raise ValueError(error_message)
        self.args['program_identifier'] = self.args['program_identifier'].to_json()
        return self
    def argument(self, arg_name, object_or_identifier):
        if not isinstance(arg_name, str):
            raise ValueError("the argument name must be a string. Was: %s" % arg_name)
        argument_dict = self.args.setdefault('argument_dict', {})
        if isinstance(object_or_identifier, FileObject):
            self.args['argument_dict'][arg_name] = object_or_identifier.identifier.to_json()
        elif isinstance(object_or_identifier, Identifier) and object_or_identifier.type == 'file':
            self.args['argument_dict'][arg_name] = object_or_identifier.to_json()
        else:
            raise ValueError("each argument of a program execution request must be a FileObject or an Identifier of a FileObject")
        return self
    def arguments(self, **kwargs):
        for arg_name, val in kwargs.items():
            self.argument(arg_name, val)
        return self


class Option:
    """
    represents an Option in the Execution Environment.
    """
    def __init__(self, identifier, description, trigger_rule, display, actions, existing_arguments, trigger=None, creator_id=None):
        self.identifier = identifier
        self.description = description
        self.trigger_rule = trigger_rule
        self.display = display
        self.actions = actions
        self.existing_arguments = existing_arguments
        self.trigger = trigger # set by the server; values provided by the user are ignored
        self.creator_id = creator_id # set by the server; values provided by the user are ignored
    def to_json(self):
        """
        gives a JSON dictionary representation of this Option.
        this is a counterpart to parse_option_request().
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'description' : self.description,
            'trigger_rule' : self.trigger_rule,
            'display' : self.display,
            'actions' : self.actions,
            'existing_arguments' : self.existing_arguments,
            'trigger' : self.trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_option(dictionary):
    """
    creates an Option from a JSON dictionary structure
    Counterpart to Option.to_json().
    """
    identifier = parse_identifier(dictionary['identifier'])
    description = dictionary['description']
    trigger_rule = dictionary['trigger_rule']
    display = dictionary['display']
    actions = dictionary['actions']
    existing_arguments = dictionary['existing_arguments']
    trigger = dictionary['trigger']
    creator_id = dictionary['creator_id']
    res = Option(identifier, description, trigger_rule, display, actions, existing_arguments, trigger=trigger, creator_id=creator_id)
    return res


def parse_object_according_to_type(type, dictionary):
    """
    helper function.
    Calls one of the other parse_x() functions, depending on the provided type.
    """
    if type == 'program':
        return parse_program(dictionary)
    elif type == 'symbol':
        return parse_symbol(dictionary)
    elif type == 'rule':
        return parse_rule(dictionary)
    elif type == 'file':
        return parse_file_object(dictionary)
    elif type == 'message':
        return parse_message(dictionary)
    elif type == 'tag':
        return parse_tag(dictionary)
    elif type == 'event':
        return parse_event(dictionary)
    elif type == 'option':
        return parse_option(dictionary)
    raise ValueException("can't parse unknown type: %s" % type)


class ObjectManager:
    """
    a manager to keep track of all of the following objects in use in an ExecutionEnvironment, as well as the relations between them:
    Program, Symbol, Rule, FileObject, Tag, Identifier.
    This acts as a single source-of-truth for all objects in use by an active ExecEnv/ExecutionEnvironment.
    It is also used to make communicating and synchronizing knowledge between server and local executions easier because this object is JSONable.
    """
    def __init__(self):
        self._current_step = None
        self._identifiers = []
        self._identifier_to_object_dict = {}
        self._identifier_to_step_number = {}
        self._identifier_to_identifier_model_id = {}
        self._tag_argument_backreferences = {}
        self._ordered_list_of_event_identifiers = []
        self._statistics_at_beginning_of_step = []
        # this mapping exists so that Tags created by a Program or a Rule can refer to objects that were created by that same Program or Rule,
        # since these objects have preliminary identifiers.
        # These get turned into proper Identifiers by the server, so this is only a temporary mapping that gets reset all the time.
        self._preliminary_identifier_mapping = {}
    def update_step_start(self, step_model):
        """
        update the step number of the object_manager and store information to be displayed later
        """
        new_step = step_model.index
        if not (self._current_step is None and new_step == 0) and not (self._current_step is not None and self._current_step == new_step - 1):
            raise ValueError("Missed a step. The current step of ObjectManager is %s but the new value is %s" % (self._current_step, new_step))
        self._current_step = new_step
        current_event = self.get_current_event()
        new_step_statistics = {
            'step' : self._current_step,
            'start_time' : step_model.created.isoformat(),
            'current_event' : None if current_event is None else current_event.identifier.to_json(),
            'signal_strengths' : self.get_signal_strengths(),
            'options_with_confidence_levels' : [{
                'option_identifier' : option.identifier.to_json(),
                'confidence' : confidence,
            } for option, confidence in self.get_options_with_confidence_levels()],
        }
        parameter_details = [('priority_of_asking_for_input', '?set_priority_of_asking_for_input', 'weight',),
                            ('priority_of_presenting_options', '?set_priority_of_presenting_options', 'weight',),
                            ('threshold_for_displaying_option', '?set_options_display_threshold', 'weight',),
                            ('threshold_for_executing_option', '?set_options_execution_threshold', 'weight',),
                            ('rule_eligibility_min_ratings_count', '?set_rule_eligibility_min_ratings_count', 'weight',),
                            ('rule_eligibility_inherit_rating', '?set_rule_eligibility_inherit_rating', 'boolean',),
                            ('rule_eligibility_requires_moderator_approval', '?set_rule_eligibility_requires_moderator_approval', 'boolean',),
                            ('rule_eligibility_always_allow_own_rules', '?set_rule_eligibility_always_allow_own_rules', 'boolean',),]
        for param_name, tag_symbol, parameter_type in parameter_details:
            current_value = self._get_value_of_parameter_from_last_tag(tag_symbol, parameter_type)
            new_step_statistics[param_name] = current_value
        self._statistics_at_beginning_of_step.append(new_step_statistics)
    def get_latest_statistics_at_beginning_of_step(self):
        """
        get the set of statistics as they were at the beginning of the most recent step.
        """
        return self._statistics_at_beginning_of_step[-1]
    def add_object(self, obj, identifier_model_id):
        """
        adds an object to this manager.
        """
        # add the mapping identifier->object
        identifier = self.get_identifier_for_object(obj)
        if identifier in self._identifier_to_object_dict:
            raise ValueError("an object with this Identifier has already been added!")
        if identifier.preliminary:
            raise ValueError("the object to be added must already have a non-preliminary Identifier!")
        self._identifier_to_object_dict[identifier] = obj
        # remember the step number at which the object was added
        self._identifier_to_step_number[identifier] = self._current_step
        # remember the ID of the database model corresponding to this Identifier
        self._identifier_to_identifier_model_id[identifier] = identifier_model_id
        # add the identifier in the right order
        self._identifiers.append(identifier)
        # if it's a Tag, add the argument backreferences
        if isinstance(obj, Tag):
            for arg_ident in obj.argument_identifiers:
                # add a backreference from the identifier of the Tag's argument to the identifier of the Tag
                if arg_ident not in self._tag_argument_backreferences:
                    self._tag_argument_backreferences[arg_ident] = []
                self._tag_argument_backreferences[arg_ident].append(identifier)
        # if it's an Event, add it to the ordered list of events
        # the position of this event in the list depends on its priority
        if isinstance(obj, Event):
            if obj.priority:
                # look down the list of events until you find the first non-priority one, then add this event before that one
                i = self._current_step
                while(True):
                    if i > len(self._ordered_list_of_event_identifiers):
                        raise ValueError("there are too few events in the ordered list for the current_step to make sense.")
                    if i == len(self._ordered_list_of_event_identifiers):
                        self._ordered_list_of_event_identifiers.append(identifier)
                        break
                    existing_event_identifier = self._ordered_list_of_event_identifiers[i]
                    existing_event = self.get_object_for_identifier(existing_event_identifier)
                    if not existing_event.priority:
                        self._ordered_list_of_event_identifiers.insert(i, identifier)
                    i += 1
            else:
                self._ordered_list_of_event_identifiers.append(identifier)
    def _overwrite_object(self, obj):
        """
        this is a hacky function that overwrites an already added object with another object.
        Do NOT use this unless you are sure.
        This function was added because it was the easiest way to resolve a circular dependency:
        An Event object needed to have a field that could only be set if the Event already had an Identifier AND that Identifier was accessible via the object_manager.
        """
        identifier = self.get_identifier_for_object(obj)
        self._identifier_to_object_dict[identifier] = obj
    def reset_preliminary_identifier_mapping(self):
        """
        resets the mapping for preliminary identifiers to real identifiers.
        """
        self._preliminary_identifier_mapping = {}
    def register_mapping_for_preliminary_identifier_to_real_identifier(self, preliminary, real):
        """
        takes a preliminary identifier and a real one and remembers that the one maps to the other.
        """
        if not preliminary.preliminary or real.preliminary or preliminary.type != real.type:
            raise ValueError('the first Identifier must be preliminary, the second not, and they must both refer to the same type:\n%s\n%s' % (preliminary.to_json(), real.to_json()))
        if preliminary in self._preliminary_identifier_mapping:
            raise ValueError("this preliminary identifier is already registered.")
        self._preliminary_identifier_mapping[preliminary] = real
    def get_real_identifier(self, identifier):
        """
        returns the real Identifier corresponding to a previously registered preliminary one.
        If the given Identifier is already a real one, and is registered, returns it instead.
        NOTE:
        If the given identifier is not registered, raises an exception.
        Because of this, using this function ensures that no Identifiers to unused objects can be created either by accident or maliciously.
        """
        if identifier in self._identifier_to_object_dict:
            return identifier
        return self._preliminary_identifier_mapping[identifier]
    def identifier_exists(self, identifier):
        """
        returns whether or not a given Identifier is already registered.
        """
        return (identifier in self._identifier_to_object_dict) or (identifier in self._preliminary_identifier_mapping)
    def get_object_for_identifier(self, identifier):
        """
        returns the object corresponding to an Identifier, if it has been registered.
        If the identifier is a preliminary one, uses the corresponding real one instead instead.
        """
        if identifier.preliminary:
            identifier = self._preliminary_identifier_mapping[identifier]
        return self._identifier_to_object_dict[identifier]
    def get_step_number_of_addition_for_identifier(self, identifier):
        """
        returns the number of the step at which an Identifier has first been registered.
        If the identifier is a preliminary one, uses the corresponding real one instead instead.
        """
        if identifier.preliminary:
            identifier = self._preliminary_identifier_mapping[identifier]
        return self._identifier_to_step_number[identifier]
    def get_identifier_model_id_for_identifier(self, identifier):
        """
        returns the ID of the IdentifierModel corresponding to an Identifier, if it has been registered.
        If the identifier is a preliminary one, uses the corresponding real one instead instead.
        """
        if identifier.preliminary:
            identifier = self._preliminary_identifier_mapping[identifier]
        return self._identifier_to_identifier_model_id[identifier]
    def get_identifier_for_object(self, obj):
        """
        returns the Identifier of an object.
        This uses the object's own identifier field, so it works even if the object is just a copy of the one that was originally added to this manager.
        """
        return obj.identifier
    def get_tag_arguments(self, tag_identifier):
        """
        returns a list of the Identifiers of arguments of a given Tag's Identifier.
        """
        tag = self.get_object_for_identifier(tag_identifier)
        res = [self.get_object_for_identifier(a) for a in tag.argument_identifiers]
        return res
    def get_tag_backreferences(self, object_identifier):
        """
        returns a list of all Tags that have the requested object as an argument.
        """
        if object_identifier not in self._tag_argument_backreferences:
            return []
        tags = [self.get_object_for_identifier(a) for a in self._tag_argument_backreferences[object_identifier]]
        for t in tags:
            if not isinstance(t, Tag):
                raise ValueError("Programming error: it should not be possible for a backreference to return something other than a list of Tags")
        return tags
    def get_all_objects(self, object_type=None, step_of_creation=None):
        """
        return a list of Objects stored in this ObjectManager.
        """
        res = []
        for identifier in self._identifiers:
            # check if the identifier matches the type
            if object_type is None:
                correct_type = True
            else:
                if not isinstance(object_type, list):
                    object_type = [object_type]
                correct_type = any([identifier.type == a for a in object_type])
            # check if the identifier matches the step
            if step_of_creation is None:
                correct_step_of_creation = True
            else:
                if not isinstance(step_of_creation, list):
                    step_of_creation = [step_of_creation]
                correct_step_of_creation = any([self.get_step_number_of_addition_for_identifier(identifier) == a for a in step_of_creation])
            # if it passed all tests, append the object to the list
            if correct_type and correct_step_of_creation:
                obj = self.get_object_for_identifier(identifier)
                res.append(obj)
        return res
    def get_current_step(self):
        """
        returns the current steo.
        """
        return self._current_step
    def get_current_event(self):
        """
        based on the current_step and the ordered_list_of_event_identifiers, return the Event that should be executed next.
        If the queue has been exceeded, returns None.
        """
        if self._current_step == len(self._ordered_list_of_event_identifiers):
            return None
        identifier = self._ordered_list_of_event_identifiers[self._current_step]
        event = self.get_object_for_identifier(identifier)
        return event
    def get_signal_strengths(self):
        """
        returns a list of signals and their strengths, for the purpose of determining which Rules to load from the server
        and in what order to test the rules for matching arguments.
        """
        # get all !set_signal_weight tags
        res_dict = {}
        for identifier in self._identifiers:
            if identifier.type == 'tag':
                tag = self.get_object_for_identifier(identifier)
                if tag.symbol_name == '!set_signal_weight':
                    target = tag.argument_identifiers[0]
                    target_symbol_id = target.id if target.type == 'symbol' else self.get_object_for_identifier(target).symbol_identifier.id
                    target_comment = tag.comment
                    target_weight_multiplier = tag.weight
                    key = (target_symbol_id, target_comment)
                    # note that newer !set_signal_weight Tags with the same symbol+comment overwrite older ones
                    res_dict[key] = target_weight_multiplier
        # don't return anything with weight 0: these values can be ignored
        res = [(target_symbol_id, target_comment, target_weight_multiplier,) for (target_symbol_id, target_comment), target_weight_multiplier \
            in res_dict.items() if target_weight_multiplier != 0]
        return res
    def get_options_with_confidence_levels(self):
        """
        returns a list of Options that could potentially be executed, along with their confidence levels.
        """
        # get all !set_option_weight tags
        res_dict = {}
        for identifier in self._identifiers:
            if identifier.type == 'tag':
                tag = self.get_object_for_identifier(identifier)
                if tag.symbol_name == '!set_option_weight':
                    target_option_identifier = tag.argument_identifiers[0]
                    target_weight = tag.weight
                    # note that newer !set_option_weight Tags for the same Option overwrite older ones
                    res_dict[target_option_identifier] = target_weight
        res = []
        for target_option_identifier, target_weight in res_dict.items():
            # ignore Options with zero weight
            if target_weight == 0:
                continue
            # ignore Options that have been deactivated
            tags_on_option_object = self.get_tag_backreferences(target_option_identifier)
            is_deactivated = any([a.symbol_name == '!deactivate_rule_or_option' for a in tags_on_option_object])
            if is_deactivated:
                continue
            res.append((self.get_object_for_identifier(target_option_identifier), target_weight,))
        return res
    def _get_value_of_parameter_from_last_tag(self, symbol_name, parameter_type):
        """
        returns the current value of a parameter, based on the last tag of that type set so far.
        """
        for identifier in reversed(self._identifiers):
            if identifier.type == 'tag':
                tag = self.get_object_for_identifier(identifier)
                if tag.symbol_name == symbol_name:
                    if parameter_type == 'weight':
                        return tag.weight
                    elif parameter_type == 'boolean':
                        comment = tag.comment
                        if comment == 'true':
                            return True
                        elif comment == 'false':
                            return False
                        else:
                            raise ValueError("a Tag of this kind must have a comment that says either 'true' or 'false'.")
                    else:
                        raise ValueError("invalid parameter type '%s'" % (parameter_type,))
        # if this value has never been set, return a nonsense default value.
        # (this shouldn't happen except at the very beginning of a scenario, before the default values have been loaded.)
        return -1.0
    def to_json(self):
        """
        gives a JSON dictionary representation of this ObjectManager that can be parsed as a new ObjectManager.
        this is a counterpart to parse_object_manager().
        """
        if len(self._preliminary_identifier_mapping.items()) != 0:
            raise ValueError("The preliminary_identifier_mapping should be reset before serializing an ObjectManager. It is only a temporary variable that shouldn't persist.")
        res = {
            'current_step' : self._current_step,
            'identifiers' : [a.to_json() for a in self._identifiers],
            'identifier_to_object_dict' : [ [k.to_json(), v.to_json()] for k,v in self._identifier_to_object_dict.items() ],
            'identifier_to_step_number' : [ [k.to_json(), v] for k,v in self._identifier_to_step_number.items()],
            'identifier_to_identifier_model_id' : [ [k.to_json(), v] for k,v in self._identifier_to_identifier_model_id.items() ],
            'tag_argument_backreferences' : [ [k.to_json(), [a.to_json() for a in v]] for k,v in self._tag_argument_backreferences.items() ],
            'ordered_list_of_event_identifiers' : [a.to_json() for a in self._ordered_list_of_event_identifiers],
            '_statistics_at_beginning_of_step' : [a for a in self._statistics_at_beginning_of_step],
        }
        return res


def parse_object_manager(dictionary):
    """
    creates an ObjectManager from a JSON dictionary structure
    Counterpart to ObjectManager.to_json().
    """
    res = ObjectManager()
    res._current_step = dictionary['current_step']
    res._identifiers = [parse_identifier(a) for a in dictionary['identifiers']]
    res._identifier_to_object_dict = { parse_identifier(kv[0]) : parse_object_according_to_type(kv[0]['type'], kv[1]) for kv in dictionary['identifier_to_object_dict'] }
    res._identifier_to_step_number = { parse_identifier(kv[0]) : kv[1] for kv in dictionary['identifier_to_step_number'] }
    res._identifier_to_identifier_model_id = { parse_identifier(kv[0]) : kv[1] for kv in dictionary['identifier_to_identifier_model_id'] }
    res._tag_argument_backreferences = { parse_identifier(kv[0]) : [parse_identifier(a) for a in kv[1]] for kv in dictionary['tag_argument_backreferences'] }
    res._ordered_list_of_event_identifiers = [parse_identifier(a) for a in dictionary['ordered_list_of_event_identifiers']]
    res._statistics_at_beginning_of_step = [a for a in dictionary['_statistics_at_beginning_of_step']]
    return res


class FeedbackRequest():
    """
    represents a FeedbackRequestModel.
    """
    def __init__(self, id, feedback_type, trigger, target_identifier, event_identifier):
        self.id = id
        self.feedback_type = feedback_type
        self.trigger = trigger
        self.target_identifier = target_identifier
        self.event_identifier = event_identifier
    def to_json(self):
        """
        gives a JSON dictionary representation of this FeedbackRequest.
        this is a counterpart to parse_feedback_request().
        """
        res = {
            'id' : self.id,
            'feedback_type' : self.feedback_type,
            'trigger' : self.trigger,
            'target_identifier' : self.target_identifier.to_json(),
            'event_identifier' : None if self.event_identifier is None else self.event_identifier.to_json(),
        }
        return res


def parse_feedback_request(dictionary):
    """
    creates a FeedbackRequest from a JSON dictionary structure
    Counterpart to FeedbackRequest.to_json().
    """
    id = dictionary['id']
    feedback_type = dictionary['feedback_type']
    trigger = dictionary['trigger']
    target_identifier = parse_identifier(dictionary['target_identifier'])
    event_identifier = None if dictionary['event_identifier'] is None else parse_identifier(dictionary['event_identifier'])
    res = FeedbackRequest(id, feedback_type, trigger, target_identifier, event_identifier)
    return res
