"""
Defines basic objects that are used by LOD scenarios.

(This file is used both on the server, and in several external applications, for consistency. As a result, some classes and/or methods may not be relevant for your purposes.)
"""
# this file should be compatible with both Python 2 and Python 3!
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from io import open
from six import string_types

import collections
import datetime
import json
import numbers
import threading
import os

from .exceptions import InvalidParamsException


##############################################################################################################
#
# 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 the file TRUTH.txt to return a dictionary of constants that are used by multiple pieces of software of the Elody corporation.
    """
    global _truth
    path = _TRUTH_FILE_LOCATION
    if _truth is None:
        with open(path, 'r') as f:
            _truth = json.load(f)
    return _truth


class Identifier(object):
    """
    A token by which objects of different types can be compared.
    All the objects in this file have a field like this.
    Identifiers can be compared using equality comparisons like == and can be used as hash keys.
    Corresponds to `this <https://elody.com/tutorial/documentation_rules/#identifier>`_.
    """
    def __init__(self, object_id, type, preliminary=None, name=None):
        self.id = object_id #: An ID that uniquely identifies the object together with the type. For Rules, Programs and Symbols, this uniquely identifies the object globally. For the other objects, it identifies them only within the current ExecutionEnvironment / Scenario.
        self.type = type #: Used together with the ID.
        if not (preliminary is None or (isinstance(preliminary, string_types) and preliminary)): # if it's a string, it must be truthy
            raise ValueError("the field 'preliminary' must be either None (if it isn't preliminary) or a String (if it is preliminary: indicates the source that created the object, so that multiple different sources can be differentiated)")
        self.preliminary = preliminary #: If this is not None, this Identifier belongs to a newly created object that has not been confirmed by the server, yet.
        self.name = name #: Used as an additional means of identifying an object. If both ID and name are given, the ID takes precedence. In the case of Rules and Programs, this name includes the version as well, like so: base_name#version
    def __str__(self):
        return "%s Identifier %s%s" % (self.type, self.id, " (preliminary from '%s')" % self.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-like dictionary representation of this identifier.
        Counterpart to :meth:`parse_identifier`.
        """
        res = {
            'type' : self.type,
            'id' : self.id,
        }
        if self.preliminary:
            res['preliminary'] = self.preliminary
        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-like dictionary structure.
    Counterpart to :meth:`Identifier.to_json`.
    Raises an InvalidParamsException if the format is incorrect.
    """
    if not isinstance(dictionary, dict):
        raise 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 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', None)
    name = dictionary.get('name', None)
    if id is None and name is None:
        raise 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 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, string_types):
        raise 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
_preliminary_identifier_source_name = None
def create_preliminary_identifier(type, name=None):
    """
    Creates a preliminary Identifier, for use by output objects.
    The Identifier has its field 'preliminary' set to a constant defined by :meth:`configure_name_for_source_of_preliminary_identifiers`.
    """
    with _preliminary_identifier_counter_lock:
        global _preliminary_identifier_counter
        c = _preliminary_identifier_counter
        _preliminary_identifier_counter += 1
    if _preliminary_identifier_source_name is None:
        raise Exception("Warning! You must call configure_name_for_source_of_preliminary_identifiers() before calling this function!")
    res = Identifier(c, type, preliminary=_preliminary_identifier_source_name, name=name)
    return res
def configure_name_for_source_of_preliminary_identifiers(name):
    """
    The server can receive preliminary Identifiers from multiple sources.
    Calling this function sets a variable that ensures that they don't accidentally overlap.
    This function must be called with a different value for each program that might create Identifiers that get parsed by the server in the same step of execution. This includes the server itself, the lod-executor, and any Programs run by it.
    The constant set by this function is used by :meth:`create_preliminary_identifier`.
    Note:
    If this is set incorrectly and some names do overlap, the server will raise an Exception.
    """
    global _preliminary_identifier_source_name
    _preliminary_identifier_source_name = name


class Program(object):
    """
    Represents `a Program <https://elody.com/tutorial/documentation_objects/#program_verifier>`_.
    """
    def __init__(self, identifier, creator_id, name, version, description, rating_numerator, rating_count):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.creator_id = creator_id #: The ID of the developer who uploaded this Program
        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-like dictionary structure from this object.
        Counterpart to :meth:`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 a Program from a JSON-like dictionary structure.
    Counterpart to :meth:`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(object):
    """
    Represents `a Symbol <https://elody.com/tutorial/documentation_objects/#symbol_verifier>`_.
    """
    def __init__(self, identifier, name, description, private=False, creator_id=None):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.name = name #:
        self.description = description #:
        self.private = private #:
        self.creator_id = creator_id #: The ID of the developer who defined this Symbol
    def to_json(self):
        """
        Creates a JSON-like dictionary structure from this object.
        Counterpart to :meth:`parse_symbol()`.
        """
        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-like dictionary structure
    Counterpart to :meth:`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(object):
    """
    Represents `a Rule <https://elody.com/tutorial/documentation_rules/#rule_verifier>`_.
    """
    def __init__(self, identifier, creator_id, name, description, dependencies, threshold, trigger, actions, existing_variables, rating_numerator, rating_count):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.creator_id = creator_id #: The ID of the developer who uploaded this Rule
        self.name = name #:
        self.description = description #:
        self.dependencies = dependencies #:
        self.threshold = threshold #:
        self.trigger = trigger #:
        self.actions = actions #:
        self.existing_variables = existing_variables #:
        self.rating_numerator = rating_numerator
        self.rating_count = rating_count
    def __str__(self):
        return "Rule %s, id %d" % (self.name, self.identifier.id,)
    def to_json(self):
        """
        Returns a JSON-like representation of the Rule object.
        Counterpart to :meth:`parse_rule()`.
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'creator_id' : self.creator_id,
            'name' : self.name,
            'description' : self.description,
            'dependencies' : self.dependencies,
            'threshold' : self.threshold,
            'trigger' : self.trigger,
            'actions' : self.actions,
            'existing_variables' : self.existing_variables,
            'rating_numerator' : self.rating_numerator,
            'rating_count' : self.rating_count,
        }
        return res


def parse_rule(dictionary):
    """
    Creates a Rule from a JSON-like dictionary structure.
    Counterpart to :meth:`Rule.to_json()`.
    """
    # the identifier
    identifier = parse_identifier(dictionary['identifier'])
    creator_id = dictionary['creator_id']
    name = dictionary['name']
    description = dictionary['description']
    dependencies = dictionary['dependencies']
    threshold = dictionary['threshold']
    trigger = dictionary['trigger']
    actions = dictionary['actions']
    existing_variables = dictionary['existing_variables']
    rating_numerator = dictionary['rating_numerator']
    rating_count = dictionary['rating_count']
    res = Rule(identifier, creator_id, name, description, dependencies, threshold, trigger, actions, existing_variables, rating_numerator, rating_count)
    return res


class FileObject(object):
    """
    Represents `a File <https://elody.com/tutorial/documentation_objects/#file_object_verifier>`_.
    This does not contain the actual content of the file, it just describes the file.
    """
    def __init__(self, identifier, file_name, creation_step, creation_index, creation_trigger=None, creator_id=None):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.file_name = file_name #:
        self.creation_step = creation_step #: This is set by the server; values provided by the user are ignored
        self.creation_index = creation_index #: This is set by the server; values provided by the user are ignored
        self.creation_trigger = creation_trigger #: This is set by the server; values provided by the user are ignored
        self.creator_id = creator_id #: This is set by the server; values provided by the user are ignored
    def __str__(self):
        return str(self.identifier)
    def to_json(self):
        """
        Gives a JSON-like dictionary representation of this FileObject that can be parsed as a new FileObject.
        Counterpart to :meth:`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['creation_trigger'] = self.creation_trigger
        res['creator_id'] = self.creator_id
        return res


def parse_file_object(dictionary):
    """
    Creates a FileObject from a JSON-like dictionary structure.
    Counterpart to :meth:`FileObject.to_json()`.
    """
    identifier = parse_identifier(dictionary['identifier'])
    file_name = dictionary['file']
    creation_step = dictionary['creation_step']
    creation_index = dictionary['creation_index']
    creation_trigger = dictionary['creation_trigger']
    creator_id = dictionary['creator_id']
    res = FileObject(identifier, file_name, creation_step, creation_index, creation_trigger=creation_trigger, creator_id=creator_id)
    return res


class Tag(object):
    """
    Represents `a Tag <https://elody.com/tutorial/documentation_objects/#tag_verifier>`_.
    A Tag consists of a Symbol, defined here as a string, and one :class:`Identifier` for each of its arguments.
    It also has an :class:`Identifier` of its own.
    Optionally, it may also have a comment and a weight.
    """
    def __init__(self, own_identifier, arguments, symbol_name=None, comment=None, weight=None, creation_trigger=None, creator_id=None):
        self.identifier = own_identifier #: The :class:`Identifier` of this object.
        self.symbol_name = symbol_name #:
        self.argument_identifiers = [a if isinstance(a, Identifier) else a.identifier for a in arguments] #:
        self.comment = comment #:
        self.weight = weight #:
        self.creation_trigger = creation_trigger #: This is set by the server; values provided by the user are ignored
        self.creator_id = creator_id #: This is 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_name, ', '.join(['%s' % a for a in self.argument_identifiers]))
    def to_json(self):
        """
        Gives a JSON-like dictionary representation of this Tag.
        Counterpart to :meth:`parse_tag()`.
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'symbol_name' : self.symbol_name,
            'argument_identifiers' : [a.to_json() for a in self.argument_identifiers],
            'comment' : self.comment,
            'weight' : self.weight,
            'creation_trigger' : self.creation_trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_tag(dictionary):
    """
    Creates a Tag from a JSON-like dictionary structure.
    Counterpart to :meth:`Tag.to_json()`.
    """
    identifier = parse_identifier(dictionary['identifier'])
    symbol_name = dictionary['symbol_name']
    argument_identifiers = [parse_identifier(a) for a in dictionary['argument_identifiers']]
    comment = dictionary['comment']
    weight = dictionary['weight']
    creation_trigger = dictionary['creation_trigger']
    creator_id = dictionary['creator_id']
    return Tag(identifier, argument_identifiers, symbol_name=symbol_name,
        comment=comment, weight=weight, creation_trigger=creation_trigger, creator_id=creator_id)


def _verify_argument_is_older_than_owner(owner_identifier, arg_identifier):
    """
    A helper function to catch an Exception early, so it doesn't get caught by the server where the error message is less clear.
    """
    if arg_identifier.preliminary is not None and owner_identifier.preliminary is not None:
        if arg_identifier.preliminary == owner_identifier.preliminary:
            if arg_identifier.id >= owner_identifier.id:
                raise ValueError("An object used as an argument must be created before the object it belongs to.")


class TagBuilder():
    """
    A class that uses method-chaining to create a :class:`Tag`.
    """
    def __init__(self):
        self.content = Tag(create_preliminary_identifier('tag'), [])
        self.also_create_signal = False
    def symbol(self, sym):
        """
        Sets the symbol of the Tag.
        The symbol can be specified either as a string, a Symbol object or an Identifier.
        """
        if isinstance(sym, string_types):
            self.content.symbol_name = sym
        elif isinstance(sym, Symbol):
            self.content.symbol_name = sym.name
        elif isinstance(sym, Identifier) and sym.type == 'symbol':
            self.content.symbol_name = sym.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):
        """
        Sets the arguments of the Tag.
        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)
        for arg_identifier in argument_identifiers:
            _verify_argument_is_older_than_owner(self.content.identifier, arg_identifier)
        self.content.argument_identifiers = argument_identifiers
        return self
    def comment(self, comment):
        """
        Sets the comment of the Tag.
        """
        if comment is not None and not isinstance(comment, string_types):
            raise ValueError("the comment must be either None or a String")
        self.content.comment = comment
        return self
    def weight(self, weight):
        """
        Sets the weight of the Tag.
        """
        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):
        """
        Gives a JSON-like dictionary representation of the constructed :class:`Tag`.
        """
        return self.content.to_json()


class Message(object):
    """
    Represents `a Message <https://elody.com/tutorial/documentation_objects/#message_verifier>`_.
    """
    def __init__(self, identifier, message_components, visibility, creation_trigger=None, creator_id=None):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.message_components = message_components #:
        self.visibility = visibility #:
        self.creation_trigger = creation_trigger #: This is set by the server; values provided by the user are ignored
        self.creator_id = creator_id #: This is set by the server; values provided by the user are ignored
    def to_json(self):
        """
        Gives a JSON-like dictionary representation of this Message that can be parsed as a new Message.
        Counterpart to :meth:`parse_message()`.
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'message_components' : self.message_components,
            'visibility' : self.visibility,
            'creation_trigger' : self.creation_trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_message(dictionary):
    """
    Creates a Message from a JSON-like dictionary structure
    Counterpart to :meth:`Message.to_json()`.
    """
    identifier = parse_identifier(dictionary['identifier'])
    message_components = dictionary['message_components']
    visibility = dictionary['visibility']
    creation_trigger = dictionary['creation_trigger']
    creator_id = dictionary['creator_id']
    res = Message(identifier, message_components, visibility, creation_trigger=creation_trigger, creator_id=creator_id)
    return res


class MessageBuilder(Message):
    """
    A class that uses method-chaining to create a :class:`Message` to the user.
    """
    def __init__(self, identifier=None, callback=None):
        super(MessageBuilder, self).__init__(create_preliminary_identifier('message') if identifier is None else identifier, [], 'all')
        self.callback = callback
    def add_text(self, message_text):
        """
        Adds a `message_component <https://elody.com/tutorial/documentation_rules/#message_component>`_ of type 'text'.
        """
        if not isinstance(message_text, string_types):
            raise ValueError("The message to display must be a string")
        component = {
            'type' : 'text',
            'text' : message_text,
        }
        self.message_components.append(component)
        return self
    def add_html(self, html_code, external_domains=None):
        """
        Adds a `message_component <https://elody.com/tutorial/documentation_rules/#message_component>`_ of type 'html'.
        """
        if not isinstance(html_code, string_types):
            raise ValueError("The HTML code must be a string")
        component = {
            'type' : 'html',
            'html' : html_code,
            'external_domains' : [] if external_domains is None else external_domains,
        }
        self.message_components.append(component)
        return self
    def add_plot(self, data, options=None):
        """
        Adds a `message_component <https://elody.com/tutorial/documentation_rules/#message_component>`_ of type 'plot'.
        """
        component = {
            'type' : 'plot',
            'data' : data,
            'options' : {} if options is None else options,
        }
        self.message_components.append(component)
        return self
    def add_downloadable_file(self, button_text, file_to_download):
        """
        Adds a `message_component <https://elody.com/tutorial/documentation_rules/#message_component>`_ of type 'downloadable_file'.
        """
        if not isinstance(file_to_download, Identifier):
            try:
                file_to_download = file_to_download.identifier
                if file_to_download.type != 'file':
                    raise Exception()
            except:
                raise ValueError("the file that is made available for download must be a file object used by LOD, or an Identifier of such a file object.")
        _verify_argument_is_older_than_owner(self.identifier, file_to_download)
        component = {
            'type' : 'downloadable_file',
            'text' : button_text,
            'file_identifier' : file_to_download.to_json(),
        }
        self.message_components.append(component)
        return self
    def add_request_for_rating(self, target, event=None):
        """
        Adds a `message_component <https://elody.com/tutorial/documentation_rules/#message_component>`_ of type 'request_for_rating'.

        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.

        The target and event of a feedback_request must be given as an :class:`Identifier` or as an object with an 'identifier' field.
        """
        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")
        _verify_argument_is_older_than_owner(self.identifier, target)
        if event is not None and not isinstance(event, 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")
        if event is not None:
            _verify_argument_is_older_than_owner(self.identifier, event)
        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 set_visibility(self, visibility):
        """
        Sets the visibility to one of 'all', 'developers', or 'hidden'.
        """
        valid_values = ['all', 'developers', 'hidden']
        if visibility not in valid_values:
            raise InvalidParamsException("The visibility must be one of: %s" % ', '.join("'%s'" % a for a in valid_values))
        self.visibility = visibility
        return self
    def finish(self):
        """
        Call this to finish building this Message.
        """
        return self.callback(self)


class Event(object):
    """
    Represents `an Event <https://elody.com/tutorial/documentation_objects/#event_verifier>`_.
    """
    def __init__(self, identifier, type, args, priority=False, triggering_step=None, creation_trigger=None, creator_id=None):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.type = type #:
        self.args = args #:
        self.priority = priority #:
        self.triggering_step = triggering_step #: This is set by the server; values provided by the user are ignored when this gets parsed by the server
        self.creation_trigger = creation_trigger #: This is set by the server; values provided by the user are ignored
        self.creator_id = creator_id #: This is 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 others.
        """
        self.priority = priority
        return self
    def to_json(self):
        """
        Gives a JSON-like dictionary representation of this Event.
        Counterpart to :meth:`parse_event()`.
        """
        res = {
            'type' : self.type,
            'identifier' : self.identifier.to_json(),
            'priority' : self.priority,
            'args' : self.args,
            'triggering_step' : self.triggering_step,
            'creation_trigger' : self.creation_trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_event(dictionary):
    """
    Creates an Event from a JSON-like dictionary structure
    Counterpart to :meth:`Event.to_json()`.
    """
    identifier = parse_identifier(dictionary['identifier'])
    event_type = dictionary['type']
    priority = dictionary['priority']
    args = dictionary['args']
    triggering_step = dictionary['triggering_step']
    creation_trigger = dictionary['creation_trigger']
    creator_id = dictionary['creator_id']
    res = Event(identifier, event_type, args, priority=priority, triggering_step=triggering_step, creation_trigger=creation_trigger, creator_id=creator_id)
    return res


class ProgramExecutionRequest(Event):
    """
    A class that uses method-chaining to create `an Event to execute a Program <https://elody.com/tutorial/documentation_objects/#event_execute_program>`_.
    """
    def __init__(self):
        super(ProgramExecutionRequest, self).__init__(create_preliminary_identifier('event'), 'execute_program', {'argument_dict' : {}, 'argument_list_dict' : {}})
    def program(self, program_identifier):
        """
        Sets the program to execute.
        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)
        """
        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=None, name=None)
        elif isinstance(program_identifier, string_types):
            self.args['program_identifier'] = Identifier(None, 'program', preliminary=None, 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):
        """
        Sets a single argument of the Program.
        The argument's name must be a string and the object must be a :class:`FileObject` or an Identifier of same.
        """
        if not isinstance(arg_name, string_types):
            raise ValueError("the argument name must be a string. Was: %s" % arg_name)
        if isinstance(object_or_identifier, Identifier):
            self.args['argument_dict'][arg_name] = object_or_identifier.to_json()
        elif hasattr(object_or_identifier, 'identifier'):
            self.args['argument_dict'][arg_name] = object_or_identifier.identifier.to_json()
        else:
            raise ValueError("each argument of a program execution request must be a valid LOD object or an Identifier of an object")
        return self
    def argument_list(self, arg_name, object_or_identifier_list):
        """
        Sets a single argument of the Program.
        The argument's name must be a string and the object must be a :class:`FileObject` or an Identifier of same.
        """
        if not isinstance(arg_name, string_types):
            raise ValueError("the argument name must be a string. Was: %s" % arg_name)
        if not isinstance(object_or_identifier_list, list):
            raise ValueError("the argument value must be a list.")
        validated_list = []
        for object_or_identifier in object_or_identifier_list:
            if isinstance(object_or_identifier, Identifier):
                validated_list.append(object_or_identifier.to_json())
            elif hasattr(object_or_identifier, 'identifier'):
                validated_list.append(object_or_identifier.identifier.to_json())
            else:
                raise ValueError("each argument_list of a program execution request must be a list of valid LOD objects or Identifiers of an object")
        self.args['argument_list_dict'][arg_name] = validated_list
        return self
    def arguments(self, **kwargs):
        """
        Sets multiple arguments or argument lists at once.
        The kwarg names become the variable names, and the kwarg values the variable values.
        """
        for arg_name, val in kwargs.items():
            if isinstance(val, list):
                self.argument_list(arg_name, val)
            else:
                self.argument(arg_name, val)
        return self


class Option(object):
    """
    Represents `an Option <https://elody.com/tutorial/documentation_objects/#option_verifier>`_.
    """
    def __init__(self, identifier, name, description, trigger, display, actions, existing_variables, creation_trigger=None, creator_id=None):
        self.identifier = identifier #: The :class:`Identifier` of this object.
        self.name = name #:
        self.description = description #:
        self.trigger = trigger #:
        self.display = display #:
        self.actions = actions #:
        self.existing_variables = existing_variables #:
        self.creation_trigger = creation_trigger #: This is set by the server; values provided by the user are ignored
        self.creator_id = creator_id #: This is set by the server; values provided by the user are ignored
    def to_json(self):
        """
        Gives a JSON-like dictionary representation of this Option.
        Counterpart to :meth:`parse_option()`.
        """
        res = {
            'identifier' : self.identifier.to_json(),
            'name' : self.name,
            'description' : self.description,
            'trigger' : self.trigger,
            'display' : self.display,
            'actions' : self.actions,
            'existing_variables' : self.existing_variables,
            'creation_trigger' : self.creation_trigger,
            'creator_id' : self.creator_id,
        }
        return res


def parse_option(dictionary):
    """
    Creates an Option from a JSON-like dictionary structure
    Counterpart to :meth:`Option.to_json()`.
    """
    identifier = parse_identifier(dictionary['identifier'])
    name = dictionary['name']
    description = dictionary['description']
    trigger = dictionary['trigger']
    display = dictionary['display']
    actions = dictionary['actions']
    existing_variables = dictionary['existing_variables']
    creation_trigger = dictionary['creation_trigger']
    creator_id = dictionary['creator_id']
    res = Option(identifier, name, description, trigger, display, actions, existing_variables, creation_trigger=creation_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(object):
    """
    A manager to keep track of all of the objects in use in a scenario, as well as the relations between them.
    This acts as a single source-of-truth for all objects in use by an active scenario. It keeps a history of when each object was added as well.

    It is also used to make communicating and synchronizing knowledge between server and local executions easier because this object has a JSON-like representation.

    When using the ObjectManager locally in your own programs, it is only useful for looking up the history of what has already happened in this scenario. On the server, it is also used for making changes. However, any changes made to a local ObjectManager are discarded by the server, so you shouldn't change anything directly. Instead, use the module :mod:`lod.lod` for making changes.
    """
    def __init__(self, execution_environment_id):
        self._execution_environment_id = execution_environment_id
        self._current_step = None
        self._next_object_id = 0
        self._identifiers = []
        self._identifier_to_object_dict = {}
        self._identifier_to_step_number = {}
        self._symbol_name_to_identifier = {}
        self._tag_argument_backreferences = {}
        self._ordered_list_of_event_identifiers = []
        self._statistics_at_beginning_of_step = []
        self._rule_ratings_when_first_loaded = {}
        # 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):
        """
        [This function is for use by the server only.]

        Update the step number of the object_manager and store information to be displayed later.
        """
        if self._current_step is None:
            self._current_step = 0
        else:
            self._current_step += 1
        current_event = self.get_current_event()
        only_trust_explicitly = False
        special_trust_for_users = []
        special_trust_for_rules = []
        for obj in self.get_all_objects():
            if isinstance(obj, Tag):
                if obj.symbol_name == '?scenario_plan_only_trust_explicitly':
                    only_trust_explicitly = (obj.weight == 1)
                if obj.symbol_name == '?scenario_plan_user_trust':
                    user_id = int(obj.comment)
                    trusted_or_distrusted = (obj.weight == 1)
                    special_trust_for_users.append((user_id, trusted_or_distrusted))
                elif obj.symbol_name == '?scenario_plan_rule_trust':
                    rule_name = obj.comment
                    rule_version = None
                    if '#' in rule_name:
                        tmp = rule_name.split('#')
                        rule_name = tmp[0]
                        rule_version = int(tmp[1])
                    trusted_or_distrusted = (obj.weight == 1)
                    special_trust_for_rules.append((rule_name, rule_version, trusted_or_distrusted))
        new_step_statistics = {
            'step' : self._current_step,
            'start_time' : datetime.datetime.now().isoformat() + 'Z', # add trailing 'Z' to explicitly mark as UTC timezone, so javascript will parse this correctly
            'current_event' : None if current_event is None else current_event.identifier.to_json(),
            'signal_strengths' : [(a, b, c) for a, b, c in self.get_signal_strengths()],
            'only_trust_explicitly' : only_trust_explicitly,
            'special_trust_for_users' : special_trust_for_users,
            'special_trust_for_rules' : special_trust_for_rules,
            '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):
        """
        [This function is for use by the server only.]

        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 get_next_object_id(self):
        """
        [This function is for use by the server only.]

        Returns a counter that can be used as the ID of newly created objects.
        """
        res = self._next_object_id
        self._next_object_id += 1
        return res
    def add_object(self, obj):
        """
        [This function is not intended to be called manually.]

        Adds an object.
        """
        # 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!")
        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
        # add the identifier in the right order
        self._identifiers.append(identifier)
        # If it's a Symbol, add the _symbol_name_to_identifier
        if isinstance(obj, Symbol):
            self._symbol_name_to_identifier[obj.name] = obj.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 function is for use by the server only.]

        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):
        """
        [This function is for use by the server only.]

        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):
        """
        [This function is for use by the server only.]

        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):
        """
        [This function is for use by the server only.]

        Returns the real Identifier corresponding to a previously registered preliminary one.
        If the given Identifier is already a real one, and is registered, returns the registered one instead
        (Note that the Identifier returned in this case may contain additional information beyond the ID and type; the returned Identifier is therefore enriched compared to the original one given as argument).
        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._preliminary_identifier_mapping:
            return self._preliminary_identifier_mapping[identifier]
        if identifier in self._identifier_to_object_dict:
            # return the match in self._identifier_to_object_dict, since it may not actually be completely identical to 'identifier'
            # (it can have additional arguments, such as the name)
            return self._identifier_to_object_dict[identifier].identifier
        raise ValueError("the Identifier is not registered with the object_manager. Are you sure you don't reference an object before creating it? This can for example happen if you first create a Message object, then a File object, then add the file to the Message for download (to fix this, create the File object before the Message object that references it.)")
    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, if it exists.
        """
        if identifier.preliminary and identifier in self._preliminary_identifier_mapping:
            identifier = self._preliminary_identifier_mapping[identifier]
        return self._identifier_to_object_dict[identifier]
    def get_step_number_of_addition(self, object_or_identifier):
        """
        Returns the number of the step at which an object has first been registered or created.
        """
        if not isinstance(object_or_identifier, Identifier):
            object_or_identifier = object_or_identifier.identifier
        if object_or_identifier.preliminary:
            object_or_identifier = self._preliminary_identifier_mapping[object_or_identifier]
        return self._identifier_to_step_number[object_or_identifier]
    def get_identifier_for_object(self, obj):
        """
        [This function is for use by the server only.]

        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_symbol_from_name(self, symbol_name):
        """
        Takes the name of a symbol as a string and returns the corresponding Symbol object if it has been loaded yet.
        If it has not been loaded yet, raises an exception.
        """
        symbol_identifier = self._symbol_name_to_identifier[symbol_name]
        return self.get_object_for_identifier(symbol_identifier)
    def find_latest_tag(self, *args, **kwargs):
        """
        Returns the latest Tag matching the specified conditions, or None if none exist.
        Uses :meth:`ObjectManager.check_tag_conditions()` to check conditions.
        """
        candidate_list = self._identifiers
        for arg in kwargs.get('arguments', []):
            # As a bit of a speedup, if any arguments are given then check their backreferences and look at only those as candidates
            if arg is not None:
                backreferences = self.get_tag_backreferences(arg)
                if len(backreferences) < len(candidate_list):
                    candidate_list = [a.identifier for a in backreferences]
        for candidate_identifier in reversed(candidate_list):
            if candidate_identifier.type != 'tag':
                continue
            candidate_tag = self._identifier_to_object_dict[candidate_identifier]
            if self.check_tag_conditions(candidate_tag, *args, **kwargs):
                return candidate_tag
        return None
    def get_tags(self, *args, **kwargs):
        """
        Returns a list of all Tags matching the specified conditions.
        Uses :meth:`ObjectManager.check_tag_conditions()` to check conditions.
        """
        candidate_list = self._identifiers
        for arg in kwargs.get('arguments', []):
            # As a bit of a speedup, if any arguments are given then check their backreferences and look at only those as candidates
            if arg is not None:
                backreferences = self.get_tag_backreferences(arg)
                if len(backreferences) < len(candidate_list):
                    candidate_list = [a.identifier for a in backreferences]
        res = []
        for candidate_identifier in candidate_list:
            if candidate_identifier.type != 'tag':
                continue
            candidate_tag = self._identifier_to_object_dict[candidate_identifier]
            if self.check_tag_conditions(candidate_tag, *args, **kwargs):
                res.append(candidate_tag)
        return res
    def check_tag_conditions(self, candidate_tag_or_identifier, symbol_name, min_weight=None, max_weight=None, comment=None, step=None, arguments=[], nullified=False):
        """
        Checks if a given Tag matches all conditions specified here. Returns True if it does, otherwise False.

        * symbol_name : The Tag's symbol must match the given string.

        * comment : The Tag's comment must match the given string exactly.

        * min_weight and max_weight : boundaries for the weight.

        * step : The Tag was created in the specified step (given as a number).

        * arguments : A list of arguments. None values are skipped. For each not-None value in this list, the Tag must have that object as an argument at that position. The objects may be given as objects or as their :class:`Identifier`.

        * nullified : If None, this is ignored. Otherwise the Tag only matches if either this argument is True and the Tag is nullified, or this argument is False and the Tag is not nullified.
        """
        if isinstance(candidate_tag_or_identifier, Identifier):
            candidate_tag = self.get_object_for_identifier(candidate_tag_or_identifier)
        else:
            candidate_tag = candidate_tag_or_identifier
        if candidate_tag.symbol_name != symbol_name:
            return False
        if min_weight is not None and candidate_tag.weight < min_weight:
            return False
        if max_weight is not None and candidate_tag.weight > max_weight:
            return False
        if comment is not None and candidate_tag.comment != comment:
            return False
        if step is not None and self.get_step_number_of_addition(candidate_tag) != step:
            return False
        arguments_match = True
        for i, arg in enumerate(arguments):
            if arg is None:
                continue
            if not isinstance(arg, Identifier):
                arg = arg.identifier
            if i >= len(candidate_tag.argument_identifiers) or candidate_tag.argument_identifiers[i] != arg:
                arguments_match = False
                break
        if not arguments_match:
            return False
        if nullified is not None:
            if nullified != self.is_nullified(candidate_tag):
                return False
        return True
    def get_tag_arguments(self, tag):
        """
        Returns a list of the Arguments of a given Tag.
        The Tag may be given either as a Tag object or as an Identifier of a Tag.
        """
        if isinstance(tag, Identifier):
            tag = self.get_object_for_identifier(tag)
        res = [self.get_object_for_identifier(a) for a in tag.argument_identifiers]
        return res
    def get_tag_backreferences(self, obj_or_identifier):
        """
        Returns an ordered list of all Tags that have the given object as an argument.
        The object may be given either as an object or as that object's Identifier.
        """
        if not isinstance(obj_or_identifier, Identifier):
            obj_or_identifier = self.get_identifier_for_object(obj_or_identifier)
        if obj_or_identifier not in self._tag_argument_backreferences:
            return []
        tags = [self.get_object_for_identifier(a) for a in self._tag_argument_backreferences[obj_or_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.
        Can be filtered by type, and by the number of the step in which the object was created.
        """
        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(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 step, as an integer.
        """
        return self._current_step
    def get_current_event(self):
        """
        Based on the current_step and the ordered_list_of_event_identifiers, return the current Event.
        If the queue of events 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 is_nullified(self, obj_or_identifier):
        """
        Returns True if the target object has a !nullify Tag on it, otherwise returns False.
        """
        if not isinstance(obj_or_identifier, Identifier):
            obj_or_identifier = obj_or_identifier.identifier
        for backreference_tag in self.get_tag_backreferences(obj_or_identifier):
            if backreference_tag.symbol_name == '!nullify':
                return True
        return False
    def is_reserved(self, obj_or_identifier, reserve_tag_symbol=None):
        """
        Checks an object to find out if there is a reserve\_ Tag targeting it in its first position.
        reserve\_ Tags with !nullify on them don't count.
        Optionally, you can specify a particular reserve\_ Tag instead of any arbitrary one.
        """
        if isinstance(obj_or_identifier, Identifier):
            obj_or_identifier = self.get_object_for_identifier(obj_or_identifier)
        # Check that the symbol, if one is specified, is a reserve_ Tag.
        if reserve_tag_symbol is not None:
            if not isinstance(reserve_tag_symbol, str) or not reserve_tag_symbol.startswith('reserve_'):
                raise InvalidParamsException("The specified Symbol does not start with 'reserve_'.")
        # Check if any tags on it are reserve_
        for t2 in self.get_tag_backreferences(obj_or_identifier):
            # Compare with reserve_tag_symbol if it exists, otherwise look for a reserve_ prefix
            match = t2.symbol_name.startswith('reserve_') if reserve_tag_symbol is None else (t2.symbol_name == reserve_tag_symbol)
            if match:
                # verify: the reserve_ actually targets the requirement as its first argument, and it is not !nullified
                if t2.argument_identifiers[0] == obj_or_identifier.identifier and not self.is_nullified(t2):
                    return True
        return False
    def rule_or_option_is_deactivated(self, rule_or_option_identifier):
        """
        Checks a Rule or Option to find out if it is marked with a non-nullified !deactivate_rule_or_option.
        Can raise an InvalidParamsException if the object is not a Rule or Option.
        """
        if not isinstance(rule_or_option_identifier, Identifier):
            rule_or_option_identifier = rule_or_option_identifier.identifier
        if rule_or_option_identifier.type not in ['rule', 'option']:
            raise InvalidParamsException("The object is neither a Rule nor an Option nor an Identifier of either.")
        for a in self.get_tag_backreferences(rule_or_option_identifier):
            if a.symbol_name == '!deactivate_rule_or_option' and not self.is_nullified(a):
                return True
        return False
    def requirement_is_provided(self, requirement_tag_or_identifier):
        """
        Checks a require\_ Tag to find out if the requirement is provided, using the !provide Tag.
        !provide Tags with !nullify on them don't count.
        Can raise an InvalidParamsException.
        """
        if isinstance(requirement_tag_or_identifier, Identifier):
            requirement_tag_or_identifier = self.get_object_for_identifier(requirement_tag_or_identifier)
        # check that it really is a require_ tag.
        if not isinstance(requirement_tag_or_identifier, Tag) or not requirement_tag_or_identifier.symbol_name.startswith('require_'):
            raise InvalidParamsException("The argument must be a tag with the prefix 'require_'.")
        # check if any tags on it are !provide
        for t2 in self.get_tag_backreferences(requirement_tag_or_identifier):
            if t2.symbol_name == '!provide':
                # verify: the !provide actually targets the requirement as its first argument, and it is not !nullified
                if t2.argument_identifiers[0] == requirement_tag_or_identifier.identifier and not self.is_nullified(t2):
                    return True
        return False
    def task_is_finished(self, task_tag_or_identifier):
        """
        Checks a task\_ Tag to find out if the task is finished.
        Looks at all require\_ Tags of the task\_ Tag. If they are all marked with !provide, the task is considered finished.
        Note that !nullify Tags can nullify both the require\_ Tags and the !provide Tags.
        Can raise an InvalidParamsException.
        """
        if isinstance(task_tag_or_identifier, Identifier):
            task_tag_or_identifier = self.get_object_for_identifier(task_tag_or_identifier)
        # check that it really is a task_ tag.
        if not isinstance(task_tag_or_identifier, Tag) or not task_tag_or_identifier.symbol_name.startswith('task_'):
            raise InvalidParamsException("The argument must be a Tag beginning with the prefix 'task_'.")
        # check if all require_ tags on it have been provided for
        for t1 in self.get_tag_backreferences(task_tag_or_identifier):
            if t1.symbol_name.startswith('require_'):
                is_nullified = self.is_nullified(t1)
                if is_nullified:
                    continue # skip any that have been !nullified
                is_provided = self.requirement_is_provided(t1)
                if not is_provided:
                    return False
        return True
    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.
        The output has the format [(target_symbol_name, target_comment, target_weight_multiplier), ...]
        """
        # 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' and not self.is_nullified(tag):
                    target = tag.argument_identifiers[0]
                    target = target.name if target.type == 'symbol' else self.get_object_for_identifier(target).symbol_name
                    target_comment = tag.comment
                    target_weight_multiplier = tag.weight
                    # note that newer !set_signal_weight Tags with the same symbol+comment overwrite older ones
                    key = (target, target_comment)
                    res_dict[key] = target_weight_multiplier
        # don't return anything with weight 0: these values can be ignored
        res = [(target, target_comment, target_weight_multiplier,) for (target, 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.
        The output has the format [(option, confidence), ...]
        """
        # get all !set_option_confidence 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_confidence':
                    target_option_identifier = tag.argument_identifiers[0]
                    target_weight = tag.weight
                    # note that newer !set_option_confidence 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 or negative weight
            if target_weight <= 0:
                continue
            # ignore Options that have been deactivated
            if self.rule_or_option_is_deactivated(target_option_identifier):
                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):
        """
        [This function is for use by the server only.]

        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-like dictionary representation of this ObjectManager that can be parsed as a new ObjectManager.
        Counterpart to :meth:`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 = {
            'execution_environment_id' : self._execution_environment_id,
            'current_step' : self._current_step,
            'next_object_id' : self._next_object_id,
            '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()],
            'symbol_name_to_identifier' : [ [k, v.to_json()] for k,v in self._symbol_name_to_identifier.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],
            'rule_ratings_when_first_loaded' : [[k,v] for k,v in self._rule_ratings_when_first_loaded.items()],
        }
        return res


def parse_object_manager(dictionary):
    """
    Creates an ObjectManager from a JSON-like dictionary structure
    Counterpart to :meth:`ObjectManager.to_json()`.
    """
    res = ObjectManager(dictionary['execution_environment_id'])
    res._current_step = dictionary['current_step']
    res._next_object_id = dictionary['next_object_id']
    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._symbol_name_to_identifier = { kv[0] : parse_identifier(kv[1]) for kv in dictionary['symbol_name_to_identifier'] }
    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']]
    res._rule_ratings_when_first_loaded = { kv[0]:kv[1] for kv in dictionary['rule_ratings_when_first_loaded'] }
    return res


class FeedbackRequest():
    """
    Represents `a Feedback Request <https://elody.com/tutorial/documentation_rules/#feedback_request>`_.
    """
    def __init__(self, id, feedback_type, creation_trigger, target_identifier, event_identifier, execution_environment_id):
        self.id = id
        self.feedback_type = feedback_type
        self.creation_trigger = creation_trigger
        self.target_identifier = target_identifier
        self.event_identifier = event_identifier
        self.execution_environment_id = execution_environment_id
    def to_json(self):
        """
        Gives a JSON-like dictionary representation of this FeedbackRequest.
        Counterpart to :meth:`parse_feedback_request()`.
        """
        res = {
            'id' : self.id,
            'feedback_type' : self.feedback_type,
            'creation_trigger' : self.creation_trigger,
            'target_identifier' : self.target_identifier.to_json(),
            'event_identifier' : None if self.event_identifier is None else self.event_identifier.to_json(),
            'execution_environment_id' : self.execution_environment_id,
        }
        return res


def parse_feedback_request(dictionary):
    """
    Creates a FeedbackRequest from a JSON-like dictionary structure
    Counterpart to :meth:`FeedbackRequest.to_json()`.
    """
    id = dictionary['id']
    feedback_type = dictionary['feedback_type']
    creation_trigger = dictionary['creation_trigger']
    target_identifier = parse_identifier(dictionary['target_identifier'])
    event_identifier = None if dictionary['event_identifier'] is None else parse_identifier(dictionary['event_identifier'])
    execution_environment_id = dictionary['execution_environment_id']
    res = FeedbackRequest(id, feedback_type, creation_trigger, target_identifier, event_identifier, execution_environment_id)
    return res
