"""The model_info module contains basic functionality for interrogating the model.

Here all functionality for interrogating and manipulating the model is hosted. For each of the elements:

 * compartments
 * species
 * parameters
 * events
 * reactions

you will find functions to add, get, set, and remove them.

"""
import tempfile

import basico
import pandas as pd

try:
    from . import model_io
except ImportError:
    import model_io

import pandas
import COPASI
import logging
import dateutil.parser
import datetime
import sys
import re

from matplotlib import pyplot as plt

logger = logging.getLogger(__name__)

MIRIAM_XML = 'http://copasi.org/static/miriam.xml' # noqa

try:
    from collections.abc import Iterable  # noqa
except ImportError:
    from collections import Iterable  # noqa

if sys.version_info[0] < 3:
    from io import open


class T:
    """Constants for Task names

    Convert between task names to enums

        >>> T.from_enum(0)
        Steady-State

        >>> T.to_enum('Steady-State')
        0

    """

    STEADY_STATE = "Steady-State"
    TIME_COURSE = "Time-Course"
    SCAN = "Scan"
    EFM = "Elementary Flux Modes"
    OPTIMIZATION = "Optimization"
    PARAMETER_ESTIMATION = "Parameter Estimation"
    MCA = "Metabolic Control Analysis"
    LYAPUNOV_EXPONENTS = "Lyapunov Exponents"
    TIME_SCALE_SEPARATION = "Time Scale Separation Analysis"
    SENSITIVITIES = "Sensitivities"
    MOIETIES = "Moieties"
    CROSS_SECTION = "Cross Section"
    LNA = "Linear Noise Approximation"
    TIME_COURSE_SENSITIVITIES = "Time-Course Sensitivities"

    _names = None
    _values = None

    @classmethod
    def _create_name_map(cls):
        return {
            COPASI.CTaskEnum.Task_steadyState: T.STEADY_STATE,
            COPASI.CTaskEnum.Task_timeCourse: T.TIME_COURSE,
            COPASI.CTaskEnum.Task_scan: T.SCAN,
            COPASI.CTaskEnum.Task_fluxMode: T.EFM,
            COPASI.CTaskEnum.Task_optimization: T.OPTIMIZATION,
            COPASI.CTaskEnum.Task_parameterFitting: T.PARAMETER_ESTIMATION,
            COPASI.CTaskEnum.Task_mca: T.MCA,
            COPASI.CTaskEnum.Task_lyap: T.LYAPUNOV_EXPONENTS,
            COPASI.CTaskEnum.Task_tssAnalysis: T.TIME_SCALE_SEPARATION,
            COPASI.CTaskEnum.Task_sens: T.SENSITIVITIES,
            COPASI.CTaskEnum.Task_moieties: T.MOIETIES,
            COPASI.CTaskEnum.Task_crosssection: T.CROSS_SECTION,
            COPASI.CTaskEnum.Task_lna: T.LNA,
            COPASI.CTaskEnum.Task_timeSens: T.TIME_COURSE_SENSITIVITIES,
        }

    @classmethod
    def _create_value_map(cls):
        return {
            T.STEADY_STATE: COPASI.CTaskEnum.Task_steadyState,
            T.TIME_COURSE: COPASI.CTaskEnum.Task_timeCourse,
            T.SCAN: COPASI.CTaskEnum.Task_scan,
            T.EFM: COPASI.CTaskEnum.Task_fluxMode,
            T.OPTIMIZATION: COPASI.CTaskEnum.Task_optimization,
            T.PARAMETER_ESTIMATION: COPASI.CTaskEnum.Task_parameterFitting,
            T.MCA: COPASI.CTaskEnum.Task_mca,
            T.LYAPUNOV_EXPONENTS: COPASI.CTaskEnum.Task_lyap,
            T.TIME_SCALE_SEPARATION: COPASI.CTaskEnum.Task_tssAnalysis,
            T.SENSITIVITIES: COPASI.CTaskEnum.Task_sens,
            T.MOIETIES: COPASI.CTaskEnum.Task_moieties,
            T.CROSS_SECTION: COPASI.CTaskEnum.Task_crosssection,
            T.LNA: COPASI.CTaskEnum.Task_lna,
            T.TIME_COURSE_SENSITIVITIES: COPASI.CTaskEnum.Task_timeSens,
        }

    @classmethod
    def from_enum(cls, int_value):
        if cls._names is None:
            cls._names = cls._create_name_map()

        return cls._names.get(int_value, T.STEADY_STATE)

    @classmethod
    def to_enum(cls, value):
        if cls._values is None:
            cls._values = cls._create_value_map()

        return cls._values.get(value, 0)

    @classmethod
    def all_task_names(cls):
        if cls._names is None:
            cls._names = cls._create_name_map()

        return list(cls._names.values())


def __status_to_int(status):
    # type: (str)->int
    codes = {
        "fixed": COPASI.CModelEntity.Status_FIXED,
        "assignment": COPASI.CModelEntity.Status_ASSIGNMENT,
        "ode": COPASI.CModelEntity.Status_ODE,
        "reactions": COPASI.CModelEntity.Status_REACTIONS,
        "time": COPASI.CModelEntity.Status_TIME,
    }
    return codes.get(status.lower(), COPASI.CModelEntity.Status_FIXED)


def __status_to_string(status):
    # type: (int)->str
    strings = {
        COPASI.CModelEntity.Status_FIXED: "fixed",
        COPASI.CModelEntity.Status_ASSIGNMENT: "assignment",
        COPASI.CModelEntity.Status_ODE: "ode",
        COPASI.CModelEntity.Status_REACTIONS: "reactions",
        COPASI.CModelEntity.Status_TIME: "time",
    }
    return strings.get(status, 'fixed')


def __xml_task_type_to_string(type_value):
    return T.from_enum(type_value)


def __task_name_to_xml_type(name):
    return T.to_enum(name)


def __line_type_to_int(line_type):
    # type: (str) -> int
    types = {
        'lines': 0,
        'points': 1,
        'symbols': 2,
        'both': 3,
        'lines_and_symbols': 3
    }
    return types.get(line_type, 0)


def __line_type_to_string(line_type):
    # type: (int) -> str
    types = {
        0: 'lines',
        1: 'points',
        2: 'symbols',
        3: 'lines_and_symbols'
    }
    return types.get(line_type, 'lines')


def __line_subtype_to_int(sub_type):
    # type: (str) -> int
    types = {
        'solid': 0,
        'dotted': 1,
        'dashed': 2,
        'dot_dash': 3,
        'dot_dot_dash': 4
    }
    return types.get(sub_type, 0)


def __line_subtype_to_string(sub_type):
    # type: (int) -> str
    types = {
        0: 'solid',
        1: 'dotted',
        2: 'dashed',
        3: 'dot_dash',
        4: 'dot_dot_dash'
    }
    return types.get(sub_type, 'solid')


def __symbol_to_int(symbol_type):
    # type: (str) -> int
    types = {
        'small_cross': 0,
        'large_cross': 1,
        'circle': 2
    }
    return types.get(symbol_type, 0)


def __symbol_to_string(symbol_type):
    # type: (int) -> str
    types = {
        0: 'small_cross',
        1: 'large_cross',
        2: 'circle'
    }
    return types.get(symbol_type, 'small_cross')


def __curve_type_to_int(curve_type):
    # type: (str) -> int
    types = {
        'curve2d': 1,
        'histoItem1d': 2,
        'bandedGraph': 3,
        'spectogram': 7
    }
    return types.get(curve_type, 1)


def __curve_type_to_string(curve_type):
    # type: (int) -> str
    types = {
        1: 'curve2d',
        2: 'histoItem1d',
        3: 'bandedGraph',
        7: 'spectogram'
    }
    return types.get(curve_type, 'curve2d')


def __function_type_to_int(function_type):
    # type: (str) -> int
    types = {
        'general': -1,
        'reversible': 1,
        'irreversible': 0
    }
    return types.get(function_type, -1)


def __function_type_to_string(function_type):
    # type: (int) -> str
    types = {
        -1: 'general',
        1: 'reversible',
        0: 'irreversible'
    }
    return types.get(function_type, 'general')


def __usage_to_int(usage):
    # type:  (str) -> int
    types = {
        "substrate": 0,
        "product": 1,
        "modifier": 2,
        "parameter": 3,
        "volume": 4,
        "time": 5,
        "variable": 6,
        "temporary": 7,
    }
    return types.get(usage.lower(), 3)


def __usage_to_string(usage):
    # type:  (int) -> str
    types = {
        0: "substrate",
        1: "product",
        2: "modifier",
        3: "parameter",
        4: "volume",
        5: "time",
        6: "variable",
        7: "temporary",
    }
    return types.get(usage, "parameter")


def get_species(name=None, exact=False, **kwargs):
    """Returns all information about the species as pandas dataframe.

    Example:

    Assume you have the brusselator example loaded `load_example('brusselator')`

        >>> get_species()

    returns you a dataframe of all species with the species name as index.

        >>> get_species('X')

    returns you only those species, that include `X` in the name.


    :param name: optional filter expression for the species, if it is not included in the species name,
                 the species will not be added to the data set.
    :type name: str

    :param exact: if true, the name has to match precisely the name of the species
    :type exact: bool

    :param kwargs: optional arguments to further filter down the species. recognized are:

     * | `model`: to specify the data model to be used (if not specified
       | the one from :func:`.get_current_model` will be taken)

     * `compartment`: to filter down only species in specific compartments

     * `type`: to filter for species of specific simulation type

    :return: a pandas dataframe with the information about the species
    :rtype: pandas.DataFrame
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    model.compileIfNecessary()

    metabs = model.getMetabolites()
    assert (isinstance(metabs, COPASI.MetabVector))

    num_metabs = metabs.size()
    data = []

    for i in range(num_metabs):
        metab = metabs.get(i)
        assert (isinstance(metab, COPASI.CMetab))

        unit = metab.getUnitExpression()
        if not unit:
            unit = model.getQuantityUnit() + '/' + model.getVolumeUnit()

        metab_data = {
            'name': metab.getObjectName(),
            'compartment': metab.getCompartment().getObjectName(),
            'type': __status_to_string(metab.getStatus()),
            'unit': unit,
            'initial_concentration': metab.getInitialConcentration(),
            'initial_particle_number': metab.getInitialValue(),
            'initial_expression': _replace_cns_with_names(metab.getInitialExpression(), model=dm),
            'expression': _replace_cns_with_names(metab.getExpression(), model=dm),
            'concentration': metab.getConcentration(),
            'particle_number': metab.getValue(),
            'rate': metab.getConcentrationRate(),
            'particle_number_rate': metab.getRate(),
            'key': metab.getKey(),
            'sbml_id': metab.getSBMLId(),
            'transition_time': metab.getTransitionTime(),
            'display_name': metab.getObjectDisplayName(),
        }

        display_name = metab.getObjectDisplayName()

        if 'name' in kwargs and not kwargs['name'] in metab_data['name']:
            continue

        if name and type(name) is str and exact and name != metab_data['name'] and name != display_name:
            continue

        if name and type(name) is str and name not in metab_data['name'] and name not in display_name:
            continue

        if name and isinstance(name, Iterable) and name not in metab_data['name'] and display_name not in name:
            continue

        if 'compartment' in kwargs and not kwargs['compartment'] in metab_data['compartment']:
            continue

        if 'type' in kwargs and kwargs['type'] not in metab_data['type']:
            continue

        if 'sbml_id' in kwargs and kwargs['sbml_id'] != metab_data['sbml_id']:
            continue

        data.append(metab_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_events(name=None, exact=False, **kwargs):
    """Returns all information about the events as pandas dataframe.

    :param name: optional filter expression for the event, if it is not included in the event name,
                 the event will not be added to the data set.
    :type name: str

    :param exact: boolean indicating whether the name has to be exact
    :type exact: bool

    :param kwargs: optional arguments:

     * | `model`: to specify the data model to be used (if not specified
       | the one from :func:`.get_current_model` will be taken)

    :return: a pandas dataframe with the information about the event
    :rtype: pandas.DataFrame
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    events = model.getEvents()

    num_events = events.size()
    data = []

    for i in range(num_events):
        event = events.get(i)
        assert (isinstance(event, COPASI.CEvent))

        assignments = []
        for j in range(event.getNumAssignments()):
            ea = event.getAssignment(j)
            assert (isinstance(ea, COPASI.CEventAssignment))
            target = ea.getTargetObject()
            if target is None:
                continue
            assignments.append(
                {
                    'target': target.getObjectDisplayName(),
                    'expression': _replace_cns_with_names(ea.getExpression(), model=dm)
                })

        event_data = {
            'name': event.getObjectName(),
            'trigger': _replace_cns_with_names(event.getTriggerExpression(), model=dm),
            'delay': _replace_cns_with_names(event.getDelayExpression(), model=dm),
            'assignments': assignments,
            'priority': _replace_cns_with_names(event.getPriorityExpression(), model=dm),
            'fire_at_initial_time': event.getFireAtInitialTime(),
            'persistent': event.getPersistentTrigger(),
            'delay_calculation': event.getDelayAssignment(),
            'key': event.getKey(),
            'sbml_id': event.getSBMLId()
        }

        if name and exact and name != event_data['name']:
            continue

        if 'name' in kwargs and not kwargs['name'] in event_data['name']:
            continue

        if name and name not in event_data['name']:
            continue

        if 'sbml_id' in kwargs and kwargs['sbml_id'] != event_data['sbml_id']:
            continue

        data.append(event_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_plots(name=None, **kwargs):
    """Returns all information about the plot definitions as pandas dataframe.

    :param name: optional filter expression for the plots, if it is not included in the plot name,
                 the plot will not be added to the data set.
    :type name: str

    :param kwargs: optional arguments:

     * | `model`: to specify the data model to be used (if not specified
       | the one from :func:`.get_current_model` will be taken)

    :return: a pandas dataframe with the information about the plot see also :func:`.get_plot_dict`
    :rtype: pandas.DataFrame
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    data = []

    for i in range(dm.getNumPlotSpecifications()):
        plot_spec = dm.getPlotSpecification(i)
        assert (isinstance(plot_spec, COPASI.CPlotSpecification))

        plot_data = get_plot_dict(plot_spec, model=dm)
        if plot_data is None:
            continue

        if 'name' in kwargs and not kwargs['name'] in plot_data['name']:
            continue

        if name and name not in plot_data['name']:
            continue

        data.append(plot_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_reports(name=None, ignore_automatic=False, task=None, **kwargs):
    """Returns the reports as dataframe

    :param name: optional filter by name
    :param ignore_automatic: if true, only manually created reports are returned
    :param task: optional task name, to the get the report for
    :param kwargs: optional arguments:

     * | `model`: to specify the data model to be used (if not specified
       | the one from :func:`.get_current_model` will be taken)

    :return: a data frame with all the report information
    :rtype: pd.DataFrame
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    data = []

    for i in range(dm.getNumReportDefinitions()):
        report = dm.getReportDefinition(i)
        assert (isinstance(report, COPASI.CReportDefinition))

        report_data = get_report_dict(report, model=dm)
        if not report_data:
            continue

        if 'name' in kwargs and not kwargs['name'] in report_data['name']:
            continue

        if ignore_automatic and 'comment' in report_data and 'Automatically generated report' in report_data['comment']:
            continue

        if task and 'task' in report_data and task not in report_data['task']:
            continue

        if name and name not in report_data['name']:
            continue

        data.append(report_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_report_dict(report, **kwargs):
    """Returns all information about the plot as dictionary

    :param report: report definition, index or name
    :type report: COPASI.CReportDefinition or int or str
    :param kwargs: optional arguments

     * | `model`: to specify the data model to be used (if not specified
       | the one from :func:`.get_current_model` will be taken)

    :return: a dictionary with all information about the plot
    :rtype: dict
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if isinstance(report, str) or isinstance(report, int):
        spec = dm.getReportDefinition(report)
        if spec is None:
            logger.error('No plot specification: {0}'.format(report))
            return

        report = spec

    assert (isinstance(report, COPASI.CReportDefinition))

    result = {
        'name': report.getObjectName(),
        'separator': report.getSeparator().getStaticString(),
        'precision': report.getPrecision(),
        'task': __xml_task_type_to_string(report.getTaskType()),
        'comment': report.getComment(),
        'is_table': report.isTable()
    }

    if report.isTable():
        result['print_headers'] = report.getTitle()
        table_items = _add_report_items_to_list([], report.getNumTableItems(), report.getNthTableItem, dm)
        result['table'] = table_items

    else:
        result['header'] = _add_report_items_to_list([], report.getNumHeaderItems(), report.getNthHeaderItem, dm)
        result['body'] = _add_report_items_to_list([], report.getNumBodyItems(), report.getNthBodyItem, dm)
        result['footer'] = _add_report_items_to_list([], report.getNumFooterItems(), report.getNthFooterItem, dm)

    return result


def _add_report_items_to_list(result, num_elements, get_nth_function, dm):
    for i in range(num_elements):
        cn = get_nth_function(i)
        obj = dm.getObject(cn)
        if obj is None:
            obj = dm.getObjectFromCN(COPASI.CCommonName(cn))

        if obj is None:
            logger.warning('report item cannot be resolved: {0}'.format(cn.getString()))
            continue

        reverse = dm.findObjectByDisplayName(obj.getObjectDisplayName())
        if reverse is None:
            # item that cannot be resolved by name so use cn
            result.append(cn.getString())
        else:
            result.append(obj.getObjectDisplayName())

    return result


def get_plot_dict(plot_spec, **kwargs):
    """Returns the information for the specified plot

        :param plot_spec: the name, index or plot specification object
        :type plot_spec: Union[str,int,COPASI.CPlotSpecification]

        :param kwargs: optional arguments

            - | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

        :return: dictionary of the form:
                | {
                |  'name': 'Phase Plot',
                |  'active': True,
                |  'log_x': False,
                |  'log_y': False,
                |  'tasks': '',
                |  'curves':
                |   [
                |     {
                |       'name': '[Y]|[X]', # the name of the curve
                |       'type': 'curve2d',# type of the curve (one of `curve2d`, `histoItem1d`, `bandedGraph`
                |                           or `spectogram`)
                |       'channels': ['[X]', '[Y]'],  # display names of all the items to be plotted
                |       'color': 'auto', # color as hex rgb value (i.e '#ff0000' for red) or 'auto'
                |       'line_type': 'lines',# the line type (one of `lines`, `points`, `symbols` or
                |                                            `lines_and_symbols`)
                |       'line_subtype': 'solid', # line subtype (one of `solid`, `dotted`, `dashed`, `dot_dash` or
                |                                 `dot_dot_dash`)
                |       'line_width': 2.0, # line width
                |       'symbol': 'small_cross', # the symbol to be used (one of `small_cross`, `large_cross`
                |                                or `circle` )
                |       'activity': 'during' # when the data should be collected (one of 'before', 'during', 'after')
                |                              from task
                |     }
                |   ]
                | }

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if isinstance(plot_spec, str) or isinstance(plot_spec, int):
        spec = dm.getPlotSpecification(plot_spec)
        if spec is None:
            logger.error('No plot specification: {0}'.format(plot_spec))
            return

        plot_spec = spec

    curves = []
    for j in range(plot_spec.getNumPlotItems()):
        plot_item = plot_spec.getItem(j)
        assert (isinstance(plot_item, COPASI.CPlotItem))
        curve_data = {
            'name': plot_item.getObjectName(),
            'type': __curve_type_to_string(plot_item.getType()),
            'channels': []
        }

        if plot_item.getParameter('Color'):
            curve_data['color'] = plot_item.getParameter('Color').getValue()
        if plot_item.getParameter('Line type'):
            curve_data['line_type'] = __line_type_to_string(plot_item.getParameter('Line type').getValue())
        if plot_item.getParameter('Line subtype'):
            curve_data['line_subtype'] = __line_subtype_to_string(plot_item.getParameter('Line subtype').getValue())
        if plot_item.getParameter('Line width'):
            curve_data['line_width'] = plot_item.getParameter('Line width').getValue()
        if plot_item.getParameter('Symbol subtype'):
            curve_data['symbol'] = __symbol_to_string(plot_item.getParameter('Symbol subtype').getValue())
        if plot_item.getParameter('Recording Activity'):
            curve_data['activity'] = plot_item.getParameter('Recording Activity').getValue()
        if plot_item.getParameter('increment'):
            curve_data['increment'] = plot_item.getParameter('increment').getValue()
        if plot_item.getParameter('logZ'):
            curve_data['log_z'] = plot_item.getParameter('logZ').getValue()
        if plot_item.getParameter('colorMap'):
            curve_data['color_map'] = plot_item.getParameter('colorMap').getValue()
        if plot_item.getParameter('bilinear'):
            curve_data['bilinear'] = plot_item.getParameter('bilinear').getValue()
        if plot_item.getParameter('contours'):
            curve_data['contours'] = plot_item.getParameter('contours').getValue()
        if plot_item.getParameter('maxZ'):
            curve_data['max_z'] = plot_item.getParameter('maxZ').getValue()

        channels = plot_item.getChannels()
        for k in range(channels.size()):
            channel_obj = dm.getObject(channels[k])
            if channel_obj is None:
                curve_data['channels'].append(channels[k].getString())
                continue
            name = channel_obj.getObjectDisplayName()
            if not dm.getObject(COPASI.CCommonName(name)):
                name = channels[k].getString()
            curve_data['channels'].append(name)

        curves.append(curve_data)

    plot_data = {
        'name': plot_spec.getObjectName(),
        'active': plot_spec.isActive(),
        'log_x': plot_spec.isLogX(),
        'log_y': plot_spec.isLogY(),
        'tasks': plot_spec.getTaskTypes(),
        'curves': curves
    }

    return plot_data


def set_plot_curves(plot_spec, curves, **kwargs):
    """Sets all curves of the named plot specification (all curves will be replaced)

        :param plot_spec: the name, index or plot specification object
        :type plot_spec: Union[str,int,COPASI.CPlotSpecification]

        :param curves: | list of dictionaries of curve items to be added. For example
              |
              | [
              |   {
              |    'name': '[Y]|[X]',          # the name of the curve
              |    'type': 'curve2d',          # type of the curve (one of `curve2d`, `histoItem1d`, `bandedGraph`
              |                                  or `spectogram`)
              |    'channels': ['[X]', '[Y]'], # display names of all the items to be plotted
              |    'color': 'auto',            # color as hex rgb value (i.e '#ff0000' for red) or 'auto'
              |    'line_type': 'lines',       # the line type (one of `lines`, `points`, `symbols` or
              |                                  `lines_and_symbols`)
              |    'line_subtype': 'solid',    #  line subtype (one of `solid`, `dotted`, `dashed`, `dot_dash` or
              |                                   `dot_dot_dash`)
              |    'line_width': 2.0,          # line width
              |    'symbol': 'small_cross',    # the symbol to be used (one of `small_cross`, `large_cross` or `circle`)
              |    'activity': 'during'   # when the data should be collected (one of 'before', 'during', 'after')
              |   }
              | ]
              |
              | Additionally, histograms may have the bin size in a `increment` element. Spectograms can have
              | `log_z`, `color_map` (one of 'Default', 'Yellow-Red', 'Grayscale' or 'Blue-White-Red'),
              | 'bilinear' (should the plot interpolate between values), 'contours' (at which values to draw contours)
              | 'max_z' (max z value).

        :type curves: [{}]

        :param kwargs: optional arguments

            - | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

        :return: None
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if isinstance(plot_spec, str) or isinstance(plot_spec, int):
        spec = dm.getPlotSpecification(plot_spec)
        if spec is None:
            logger.error('No plot specification: {0}'.format(plot_spec))
            return

        plot_spec = spec

    assert (isinstance(plot_spec, COPASI.CPlotSpecification))

    plot_spec.cleanup()

    if not curves:
        return

    for curve in curves:
        name = curve['name'] if 'name' in curve else 'curve_{0}'.format(plot_spec.getNumPlotItems())
        curve_type = __curve_type_to_int(curve['type']) if 'type' in curve else 1

        plot_item = plot_spec.createItem(name, curve_type)
        assert (isinstance(plot_item, COPASI.CPlotItem))

        color = curve['color'] if 'color' in curve else 'auto'
        if plot_item.getParameter('Color'):
            plot_item.getParameter('Color').setStringValue(color)
        line_subtype = __line_subtype_to_int(curve['line_subtype']) if 'line_subtype' in curve else 0
        if plot_item.getParameter('Line subtype'):
            plot_item.getParameter('Line subtype').setValue(line_subtype)
        line_type = __line_type_to_int(curve['line_type']) if 'line_type' in curve else 0
        if plot_item.getParameter('Line type'):
            plot_item.getParameter('Line type').setValue(line_type)
        line_width = curve['line_width'] if 'line_width' in curve else 2.0
        if plot_item.getParameter('Line width'):
            plot_item.getParameter('Line width').setValue(line_width)
        symbol = __symbol_to_int(curve['symbol']) if 'symbol' in curve else 0
        if plot_item.getParameter('Symbol subtype'):
            plot_item.getParameter('Symbol subtype').setValue(symbol)
        activity = curve['activity'] if 'activity' in curve else 'during'
        if plot_item.getParameter('Recording Activity'):
            plot_item.getParameter('Recording Activity').setStringValue(activity)

        if plot_item.getParameter('increment') and 'increment' in curve:
            plot_item.getParameter('increment').setValue(curve['increment'])
        if plot_item.getParameter('logZ') and 'log_z' in curve:
            plot_item.getParameter('logZ').setValue(curve['log_z'])
        if plot_item.getParameter('colorMap') and 'color_map' in curve:
            plot_item.getParameter('colorMap').setValue(curve['color_map'])
        if plot_item.getParameter('bilinear') and 'bilinear' in curve:
            plot_item.getParameter('bilinear').setValue(curve['bilinear'])
        if plot_item.getParameter('contours') and 'contours' in curve:
            plot_item.getParameter('contours').setValue(curve['contours'])
        if plot_item.getParameter('maxZ') and 'max_z' in curve:
            plot_item.getParameter('maxZ').setValue(curve['max_z'])

        if 'channels' in curve:
            for channel in curve['channels']:
                if channel.startswith('CN='):
                    plot_item.addChannel(COPASI.CPlotDataChannelSpec(COPASI.CCommonName(channel)))
                    continue

                obj = dm.findObjectByDisplayName(channel)
                if channel == 'Time':
                    obj = dm.getModel().getValueReference()
                if obj is None:
                    logger.warning("Couldn't resolve {0} when adding curve spec".format(channel))
                    continue
                plot_item.addChannel(COPASI.CPlotDataChannelSpec(obj.getCN()))


def add_plot(name, **kwargs):
    """Adds a new plot specification to the model.

        :param name: the name for the new plot specification
        :type name: str

        :param kwargs: optional parameters, recognized are:

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

            * all other parameters from :func:`set_plot_dict`.

        :return: the plot
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))
    plot_spec = dm.getPlotDefinitionList().createPlotSpec(name, COPASI.CPlotItem.plot2d)

    if not plot_spec:
        raise ValueError('A plot named ' + name + ' already exists')

    set_plot_dict(plot_spec, **kwargs)

    return plot_spec


def add_default_plot(name, **kwargs):
    """Adds a default plot to the list of plots

    :param name: name of the default plot
    :type name: str


    :param kwargs: optional arguments

        - | `new_name`: to rename the plot specification

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: none or the name of the plot created
    :rtype: str or None
    """

    if name not in get_default_plot_names():
        logger.warning('No such default plot: {0}'.format(name))
        return

    item = COPASI.COutputAssistant.getItem(_default_plots[name])
    assert (isinstance(item, COPASI.CDefaultOutputDescription))

    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    for task in dm.getTaskList():
        assert (isinstance(task, COPASI.CCopasiTask))
        if task.getType() == item.mTaskType:
            dm.getModel().compileIfNecessary()
            if isinstance(task, COPASI.CFitTask):
                task.getProblem().getExperimentSet().compile(task.getMathContainer())
            task.initializeRaw(COPASI.CCopasiTask.OUTPUT_UI)
            result = COPASI.COutputAssistant.createDefaultOutput(_default_plots[name], task, dm)
            if result is None:
                logger.warning('Failed to create default plot for: {0}'.format(name))
                return None
            return result.getObjectName()

    logger.warning('No task found for the plot')
    return None


_default_plots = {}


def get_default_plot_names(filter=None, **kwargs):
    """Returns a list of default plot names

    :param filter: optional filter of substring to be in the name
    :param kwargs:
    :return:
    """
    ids = COPASI.COutputAssistant.getListOfDefaultOutputDescriptions()
    global _default_plots
    if _default_plots:
        return _default_plots

    _default_plots = {}
    for index in ids:
        item = COPASI.COutputAssistant.getItem(index)
        assert (isinstance(item, COPASI.CDefaultOutputDescription))
        if not item.isPlot:
            continue
        if filter and filter not in item.name:
            continue
        _default_plots[item.name] = index
    return _default_plots


def set_plot_dict(plot_spec, active=True, log_x=False, log_y=False, tasks='', **kwargs):
    """Sets properties of the named plot specification.

        :param plot_spec: the name, index or plot specification object
        :type plot_spec: Union[str,int,COPASI.CPlotSpecification]

        :param active: boolean indicating whether plot should be active (defaults to true)
        :type active: bool

        :param log_x: boolean indicating that the x axis should be logarithmic
        :type log_x: bool

        :param log_y: boolean indicating that the y axis should be logarithmic
        :type log_y: bool

        :param tasks: | task type (or colon separated list of task types) for which the plot
                      | should be brought up
        :type tasks: str

        :param kwargs: optional arguments

            - | `new_name`: to rename the plot specification

            - | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

            - `curves`: dictionary in the format as described in :func:`set_plot_curves`.

        :return: None
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if isinstance(plot_spec, str) or isinstance(plot_spec, int):
        spec = dm.getPlotSpecification(plot_spec)
        if spec is None:
            logger.error('No plot specification: {0}'.format(plot_spec))
            return

        plot_spec = spec

    assert (isinstance(plot_spec, COPASI.CPlotSpecification))

    plot_spec.setActive(active)
    plot_spec.setLogX(log_x)
    plot_spec.setLogY(log_y)
    plot_spec.setTaskTypes(tasks)

    if 'curves' in kwargs:
        set_plot_curves(plot_spec, **kwargs)

    if 'new_name' in kwargs:
        plot_spec.setObjectName(kwargs['new_name'])


def add_report(name, **kwargs):
    """Adds a new report specification to the model.

     Examples:

        The following would adds a report definition 'Time Course' to include Time and the concentration
        of S, in a report that is separated by tabs.

        >>> add_report('Time Course', body=['Time', '[S]']

        The following defines a report for the Steady State concentration of S. To disambiguate, that the
         string '[S]' in the header should be used literally, we call the function wrap_copasi_string.

        >>> add_report('Steady State', task=T.STEADY_STATE, header=[wrap_copasi_string('[S]')], footer=['[S]'])

        :param name: the name for the new plot specification
        :type name: str

        :param kwargs: optional parameters, recognized are:

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

            * all other parameters from :func:`set_report_dict`.

        :return: the report definition
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))
    spec = dm.getReportDefinitionList().createReportDefinition(name, "")

    if not spec:
        raise ValueError('A report named ' + name + ' already exists')

    set_report_dict(spec, **kwargs)

    return spec


def set_report_dict(spec, precision=None, separator=None, table=None,
                    print_headers=True, header=None, body=None, footer=None, task=None,
                    comment=None, add_separator=None, **kwargs):
    """Sets properties of the named report definition.

        Examples:

        The following would set a report definition 'Time Course' to include Time and the concentration
        of S, in a report that is separated by tabs.

        >>> set_report_dict('Time Course', body=['Time', '[S]']

        The following defines a report for the Steady State concentration of S. To disambiguate, that the
         string '[S]' in the header should be used literally, we call the function wrap_copasi_string.

        >>> set_report_dict('Steady State', task=T.STEADY_STATE, header=[wrap_copasi_string('[S]')], footer=['[S]'])


        :param spec: the name, index or report definition object
        :type spec: Union[str,int,COPASI.CReportDefinition]

        :param precision: number of digits to print (defaults to 6)
        :type precision: Optional[int]

        :param separator: the separator to use between elements (defaults to \t)
        :type separator: Optional[str]

        :param table: a list of CNs or display names of elements to collect in a table. If `table` is specified
              the header, body, footer argument will be ignored. Note that setting table elements is only
              useful for tasks that generate output *during* the task. If that is not the case, you will have
              to specify the footer and header element directly.

        :type table: [str]

        :param print_headers: optional arguments, indicating whether table headers will be printed (only applies
               when the `table` argument is given
        :type print_headers: bool

        :param header: a list of CNs or display names of elements to collect in the header column
        :type header: [str]

        :param body: a list of CNs or display names of elements to collect in the body rows
        :type body: [str]

        :param footer: a list of CNs or display names of elements to collect in the footer column
        :type footer: [str]

        :param task: | task name for which the report should be used
        :type task: Optional[str]

        :param comment: a documentation string for the report (can bei either string, or xhtml string)
        :type comment: Optional[str]

        :param add_separator: an optional boolean flag, to automatically add seprators between header, body and
            footer entries since this is not necessary for table entries.
        :type add_separator: Optional[bool]

        :param kwargs: optional arguments

            - | `new_name`: to rename the report

            - | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

        :return: None
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if isinstance(spec, str) or isinstance(spec, int):
        r = dm.getReportDefinition(spec)
        if r is None:
            logger.error('No report specification: {0}'.format(spec))
            return

        spec = r

    assert (isinstance(spec, COPASI.CReportDefinition))

    if 'new_name' in kwargs:
        spec.setObjectName(kwargs['new_name'])

    if comment:
        spec.setComment(comment)

    if precision:
        spec.setPrecision(precision)

    if separator:
        spec.setSeparator(separator)

    if task is not None:
        spec.setTaskType(__task_name_to_xml_type(task))

    if table:
        spec.setIsTable(True)
        spec.setTitle(print_headers)
        _set_report_vector(spec.getTableAddr(), table, dm)
    else:
        seprator = 'Separator=' + spec.getSeparator().getStaticString()
        if header:
            _set_report_vector(spec.getHeaderAddr(), header, dm, seprator)
            spec.setIsTable(False)
        if body:
            _set_report_vector(spec.getBodyAddr(), body, dm, seprator)
            spec.setIsTable(False)
        if footer:
            _set_report_vector(spec.getFooterAddr(), footer, dm, seprator)
            spec.setIsTable(False)


def _set_report_vector(vec, list_of_cns, dm, separator=None):
    """ Sets the given vector to the elements of the list

    :param vec: the vector to change
    :type vec: COPASI.ReportItemVector
    :param list_of_cns: list of cns or display names to add
    :type list_of_cns: [str]
    :param dm: the data model to resolve elements in
    :type dm: COPASI.CDataModel
    :param separator: optional sepratator string to add between entries
    :type separator: Optional[str]
    :return: None
    """
    vec.clear()

    if not list_of_cns:
        return

    for item in list_of_cns:
        if item.startswith('CN'):
            vec.append(COPASI.CRegisteredCommonName(item))
            if separator:
                vec.append(COPASI.CRegisteredCommonName(separator))
            continue

        obj = dm.findObjectByDisplayName(item)
        if obj:
            if obj.getObjectType() != 'Reference':
                obj = obj.getValueReference()
            vec.append(COPASI.CRegisteredCommonName(obj.getCN()))
            if separator:
                vec.append(COPASI.CRegisteredCommonName(separator))
            continue

        if not item.startswith('String=') and not item.startswith('Separator='):
            item = wrap_copasi_string(item)
        vec.append(COPASI.CRegisteredCommonName(item))
        if separator:
            vec.append(COPASI.CRegisteredCommonName(separator))

    if separator:
        vec.pop()


def wrap_copasi_string(text):
    """ Utility function wrapping the given text into a COPASI string

    :param text: the text to wrap
    :return: an escaped COPASI string, that can be used in reports
    """
    return 'String=' + COPASI.CCommonName.escape(text)

def _is_known_reference_start(text):
    if text.startswith('['):
        return True
    if text.startswith('Values['):
        return True
    if text.startswith('('):
        return True
    return False

def _is_known_reference_end(text):
    for end in [']', 
                '].InitialValue', 
                '].Value',
                '].Rate',
                '].InitialConcentration',
                ]:
        if text.endswith(end):
            return True
    return False

def _replace_names_with_cns(expression, **kwargs):
    """ replaces all names in the given expression with cns

    :param expression: the expression as infix
    :type expression: str
    :param kwargs:
    :return: the expression with names replaced by cns
    """

    if type(expression) is not str:
        expression = str(expression)

    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))
    resulting_expression = ''
    expression = expression.replace('{', ' {')
    expression = expression.replace('}', '} ')
    current_word = None

    for word in expression.split():
        if word.startswith('{') and word.endswith('}'):
            if current_word is not None:
                resulting_expression += ' ' + current_word
                current_word = None
            word = word[1:-1]
        else:
            is_known = dm.findObjectByDisplayName(word)
            if _is_known_reference_start(word) and not is_known:
                if current_word is not None:
                    resulting_expression += ' ' + current_word
                current_word = word
                continue

            if current_word is not None:
                current_word += ' ' + word
                if _is_known_reference_end(word):
                    word = current_word
                    current_word = None
                elif dm.findObjectByDisplayName(word) is not None:
                    word = current_word
                    current_word = None
                else:
                    continue

        obj = dm.findObjectByDisplayName(word)
        if obj is not None:
            if isinstance(obj, COPASI.CModel):
                obj = obj.getValueReference()
            resulting_expression += ' <{0}>'.format(obj.getCN())
        else:
            resulting_expression += ' ' + word

    return resulting_expression.strip()


def _split_by_cn(expression):
    result = []
    current = ''
    num_chars = len(expression)
    pos = 0
    while pos < num_chars:
        has_more = pos + 4 < num_chars
        cur_char = expression[pos]

        if cur_char == '<' and has_more:

            next_3 = expression[pos:pos+4]

            if next_3.startswith('<CN='):
                end = expression.find('>', pos)
                cn = expression[pos+1: end]
                result.append(cn)
                pos = end + 1
                current = ''
                continue

        if cur_char in '/*+-()^%<>!=&|':

            if current:
                result.append(current)
                current = ''

            if pos + 1 < num_chars and expression[pos + 1] == '=':
                cur_char += '='
                pos += 1
            result.append(cur_char)

        elif cur_char != ' ':
            current += cur_char

        pos += 1

    if current:
        result.append(current)

    return result


def _replace_cns_with_names(expression, **kwargs):
    if not expression:
        return expression

    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))
    resulting_expression = ''
    words = _split_by_cn(expression)
    skip = -1
    for i in range(len(words)):
        if i < skip:
            continue

        word = words[i]
        if word.startswith('<CN') and word.endswith('>'):
            word = word[1:-1]
            cn = word
            word = ''
        elif word.startswith('<CN'):
            cn = word[1:]
            i = i + 1
            while not words[i].endswith('>'):
                cn += ' ' + words[i]
                i = i + 1
            cn += ' ' + words[i][:-1]
            i = i + 1
            skip = i
            word = ''
        elif word.startswith('CN='):
            cn = word
            word = ''
        else:
            cn = None

        if cn is not None:
            obj = dm.getObject(COPASI.CCommonName(cn))
            if obj is not None:
                word = obj.getObjectDisplayName()
        resulting_expression += ' ' + word

    return resulting_expression.strip()


def set_notes(notes, **kwargs):
    """Sets notes on the provided element

    :param notes: the notes to be set, can be either plain text, or valid xhtml
    :type notes: str

    :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)
        * | `name`: the display name of the element to set the notes on.
          | otherwise the main model will be taken.
        * | `element`: any model element

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    element = dm.getModel()
    if 'element' in kwargs:
        element = kwargs['element']
    elif 'name' in kwargs:
        element = dm.findObjectByDisplayName(kwargs['name'])
        if element is None:
            logger.warning("Couldn't find element {0} to set notes.".format(kwargs['name']))
            return

    if element is None:
        logger.warning("Couldn't find element to set notes.")
        return

    if isinstance(element, COPASI.CDataObject):
        if element.getObjectType() == 'Reference':
            element = element.getObjectParent()
        element = COPASI.CAnnotation.castObject(element)

    if isinstance(element, COPASI.CAnnotation):
        element.setNotes(notes)
    else:
        logger.warning("Unsupported element type for setting notes.")


def get_notes(**kwargs):
    """Returns all notes on the element or model.

    :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)
        * | `name`: the display name of the element to set the notes on.
          | otherwise the main model will be taken.
        * | `element`: any model element

    :return: the notes string (plain text, or xhtml)
    :rtype: str
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    element = dm.getModel()
    if 'element' in kwargs:
        element = kwargs['element']
    elif 'name' in kwargs:
        element = dm.findObjectByDisplayName(kwargs['name'])
        if element is None:
            logger.warning("Couldn't find element {0} to get notes.".format(kwargs['name']))
            return None

    if element is None:
        logger.warning("Couldn't find element to get notes.")
        return None

    if isinstance(element, COPASI.CDataObject):
        if element.getObjectType() == 'Reference':
            element = element.getObjectParent()
        element = COPASI.CAnnotation.castObject(element)

    if isinstance(element, COPASI.CAnnotation):
        return element.getNotes()
    else:
        logger.warning("Unsupported element type for getting notes.")
    return None


def get_miriam_annotation(**kwargs):
    """Returns the elements miriam annotations as dictionary of the form:

    {
        'created': datetime,
        'creators': [{
            'first_name': '...',
            'last_name': '...',
            'email': '...',
            'organization': '...'
        },...],

        'references': [{
            'id': '...',
            'uri': 'identifiers.org uri',
            'resource': 'human readable name of resource',
            'description', '...'
        },...],

        'descriptions': [{
            'id': '...',
            'qualifier': 'human readable qualifier string',
            'uri': 'identifiers.org uri',
            'resource': 'name of the resource referenced'
        },...],
        'modifications': [datetime,...]
    }

    :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)
        * | `name`: the display name of the element to set the notes on.
          | otherwise the main model will be taken.
        * | `element`: any model element

    :return: the elements annotation as dictionary as described
    :rtype: {}
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    element = dm.getModel()
    if 'element' in kwargs:
        element = kwargs['element']
    elif 'name' in kwargs:
        element = dm.findObjectByDisplayName(kwargs['name'])
        if element is None:
            logger.warning("Couldn't find element {0} to get annotations.".format(kwargs['name']))
            return None

    if element is None:
        logger.warning("Couldn't find element to get annotations.")
        return None

    if isinstance(element, COPASI.CDataObject):
        info = COPASI.CMIRIAMInfo()
        info.load(element)
        result = {'creators': [], 'references': [],
                  'descriptions': [], 'modifications': []}

        try:
            dt = dateutil.parser.isoparse(info.getCreatedDT())
            result['created'] = dt
        except ValueError:
            pass

        for creator in info.getCreators():
            result['creators'].append({
                'first_name': creator.getGivenName(),
                'last_name': creator.getFamilyName(),
                'email': creator.getEmail(),
                'organization': creator.getORG()
            })

        for reference in info.getReferences():
            result['references'].append({
                'id': reference.getId(),
                'uri': reference.getMIRIAMResourceObject().getIdentifiersOrgURL(),
                'resource': reference.getResource(),
                'description': reference.getDescription()
            })

        for description in info.getBiologicalDescriptions():
            assert (isinstance(description, COPASI.CBiologicalDescription))
            result['descriptions'].append({
                'id': description.getId(),
                'qualifier': description.getPredicate(),
                'uri': description.getMIRIAMResourceObject().getIdentifiersOrgURL(),
                'resource': description.getResource(),
            })

        for modification in info.getModifications():
            assert (isinstance(modification, COPASI.CModification))
            result['modifications'].append(dateutil.parser.isoparse(modification.getDate()))

        for key in ['creators', 'references', 'descriptions', 'modifications']:
            if len(result[key]) == 0:
                del result[key]

        return result
    else:
        logger.warning("Unsupported element type for getting annotations.")
    return None


def set_miriam_annotation(created=None, creators=None, references=None, descriptions=None, modifications=None,
                          replace=True,
                          **kwargs):
    """Sets the MIRIAM annotations for the provided element or model

    :param created: the date/time to set as the objects creation time or None, if not to be set
    :type created: datetime.datetime or None

    :param creators: | None, if not to be modified, otherwise list of creators of the form:
                     | {
                     |  'first_name': '...',
                     |  'last_name': '...',
                     |  'email': '...',
                     |  'organization': '...'
                     | }
    :type creators: list or None

    :param references: | None if not to be modified, otherwise list of references of the form:
                       |
                       | {
                       |   'resource': 'human readable name of resource',
                       |   'id': '...',
                       |   'uri': 'identifiers.org uri',
                       |   'description', '...'
                       | }
                       |
                       | only the uri needs to be provided, or alternatively id + resource.
    :type references: list or None

    :param descriptions: | None if not to be modified, otherwise list of descriptions of the form:
                         |
                         | {
                         |   'resource': 'human readable name of resource',
                         |   'id': '...',
                         |   'qualifier': '...',
                         |   'uri': 'identifiers.org uri',
                         | }
                         |
                         | only the uri needs to be provided, or alternatively id + resource.

    :type descriptions: list or None

    :param modifications: | None if not to be modified, otherwise list of datetime objects representing
                          | modification dates
    :type modifications: list or None

    :param replace: Boolean indicating whether existing entries should be removed.
    :type replace: bool

        :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)
        * | `name`: the display name of the element to set the notes on.
          | otherwise the main model will be taken.
        * | `element`: any model element

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    element = dm.getModel()
    if 'element' in kwargs:
        element = kwargs['element']
    elif 'name' in kwargs:
        element = dm.findObjectByDisplayName(kwargs['name'])
        if element is None:
            logger.warning("Couldn't find element {0} to set annotations.".format(kwargs['name']))
            return

    if element is None:
        logger.warning("Couldn't find element to set annotations.")
        return

    if not isinstance(element, COPASI.CDataObject):
        logger.warning("Unsupported element type for setting annotations.")
        return

    info = COPASI.CMIRIAMInfo()
    info.load(element)

    if created is not None and isinstance(created, datetime.datetime):
        info.setCreatedDT(created.isoformat())

    if creators is not None:
        if replace:
            _remove_creators(info)
        for creator in creators:
            c = info.createCreator('')
            assert (isinstance(c, COPASI.CCreator))
            if 'first_name' in creator:
                c.setGivenName(creator['first_name'])
            if 'last_name' in creator:
                c.setFamilyName(creator['last_name'])
            if 'email' in creator:
                c.setEmail(creator['email'])
            if 'organization' in creator:
                c.setORG(creator['organization'])

    if references is not None:
        if replace:
            _remove_references(info)
        for reference in references:
            r = info.createReference('')
            assert (isinstance(r, COPASI.CReference))
            if 'resource' in reference:
                r.setResource(reference['resource'])
            if 'id' in reference:
                r.setId(reference['id'])
            if 'description' in reference:
                r.setDescription(reference['description'])
            if 'uri' in reference:
                r.getMIRIAMResourceObject().setURI(reference['uri'])

    if descriptions is not None:
        if replace:
            _remove_descriptions(info)
        for desc in descriptions:
            d = info.createBiologicalDescription()
            assert (isinstance(d, COPASI.CBiologicalDescription))
            if 'qualifier' in desc:
                d.setPredicate(desc['qualifier'])
            if 'resource' in desc:
                d.setResource(desc['resource'])
            if 'id' in desc:
                d.setId(desc['id'])
            if 'uri' in desc:
                d.getMIRIAMResourceObject().setURI(desc['uri'])

    if modifications is not None:
        if replace:
            _remove_modifications(info)
        for modification in modifications:
            m = info.createModification('')
            assert (isinstance(m, COPASI.CModification))
            m.setDate(modification.isoformat())

    info.save()


def _remove_modifications(info):
    num_entries = info.getModifications().size()
    for _ in range(num_entries):
        info.removeModification(info.getModifications().get(0))


def _remove_descriptions(info):
    num_entries = info.getBiologicalDescriptions().size()
    for _ in range(num_entries):
        info.removeBiologicalDescription(info.getBiologicalDescriptions().get(0))


def _remove_references(info):
    num_entries = info.getReferences().size()
    for _ in range(num_entries):
        info.removeReference(info.getReferences().get(0))


def _remove_creators(info):
    num_entries = info.getCreators().size()
    for _ in range(num_entries):
        info.removeCreator(info.getCreators().get(0))


def add_compartment(name, initial_size=1.0, **kwargs):
    """Adds a new compartment to the model.

    :param name: the name for the new compartment
    :type name: str

    :param initial_size: the initial size for the compartment
    :type initial_size: float

    :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

        * all other parameters from :func:`set_compartment`.

    :return: the compartment added
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    compartment = model.createCompartment(name, initial_size)
    if compartment is None:
        raise ValueError('A compartment named ' + name + ' already exists')

    _set_compartment(compartment, model, **kwargs)

    return compartment


def add_function(name, infix, type='general', mapping=None, **kwargs):
    """Adds a new function definition if none with that name already exists

    :param name: the name for the new function
    :type name: str

    :param infix: the formula for the new function (e.g: `V * S / ( K + S )`)
    :type infix: str

    :param type: optional flag specifying whether the function is
                 'reversible', 'irreversible' or 'general'
    :type type: str

    :param mapping: optional dictionary mapping the elements of the infix
           to their usage. If not specified, the usage will default to `parameter`,
           other values possible would be `substrate`, `product`, `modifier`, `volume or
           `time`. One example for the infix for the infix above  we would specify that
           `S` is `substrate`:

           { 'S': 'substrate' }

    :type mapping: dict

    :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    """
    root = COPASI.CRootContainer.getRoot()
    assert (isinstance(root, COPASI.CRootContainer))
    db = root.getFunctionList()
    assert (isinstance(db, COPASI.CFunctionDB))

    if mapping is None:
        mapping = {}

    if db.findLoadFunction(name) is not None:
        logger.error('A function with name "' + name + '" already exists')
        return

    fun = db.createFunction(name, COPASI.CEvaluationTree.Function)
    if fun is None:
        logger.error('Could not create a function with name "' + name + '" already exists')
        return

    fun.setInfix(infix)
    fun.setReversible(__function_type_to_int(type))

    variables = fun.getVariables()
    assert (isinstance(variables, COPASI.CFunctionParameters))

    for i in range(variables.size()):
        param = variables.getParameter(i)
        assert (isinstance(param, COPASI.CFunctionParameter))
        usage = __usage_to_int(mapping.get(param.getObjectName(), 'parameter'))
        param.setUsage(usage)

    return fun


def remove_function(name, **kwargs):
    """Removes the function with the given name

    :param name: the name of the function to be removed
    :type name: str

    :param kwargs: optional parameters, recognized are:

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:
    """
    root = COPASI.CRootContainer.getRoot()
    assert (isinstance(root, COPASI.CRootContainer))
    db = root.getFunctionList()
    assert (isinstance(db, COPASI.CFunctionDB))

    fun = db.findFunction(name)
    assert (isinstance(fun, COPASI.CFunction))
    if fun is None:
        logger.warning('A function with name "' + name + '" does not exists')
        return

    if fun.isReadOnly():
        logger.error('The function "' + name + '" is readonly and cannot be deleted')
        return

    key = fun.getKey()

    db.removeFunction(key)


def remove_user_defined_functions():
    """Removes all user defined functions along with all elements that still use them
    """
    root = COPASI.CRootContainer.getRoot()
    assert (isinstance(root, COPASI.CRootContainer))
    db = root.getFunctionList()
    assert (isinstance(db, COPASI.CFunctionDB))
    funs = db.loadedFunctions()

    to_be_deleted = []

    for i in range(funs.size()):
        fun = funs.get(i)
        if fun is None:
            continue
        if fun.isReadOnly():
            continue
        to_be_deleted.append(fun.getKey())

    for key in to_be_deleted:
        db.removeFunction(key)


def add_species(name, compartment_name='', initial_concentration=1.0, **kwargs):
    """Adds a new species to the model.

        :param name: the name for the new species
        :type name: str

        :param compartment_name: optional the name of the compartment in which the species should be
               created, it will default to the first compartment. If no compartment is present,
               a unit compartment named `compartment` will be created.
        :type compartment_name: str

        :param initial_concentration: optional the initial concentration of the species
        :type initial_concentration: float

        :param kwargs: optional parameters, recognized are:

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)
            * all other parameters from :func:`set_species`.

        :return: the newly created species
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    # create compartment if it does not yet exist
    if not compartment_name:
        if model.getNumCompartments() == 0:
            model.createCompartment('compartment', 1)
        compartment_name = model.getCompartment(0).getObjectName()
    elif not model.getCompartment(compartment_name):
        model.createCompartment(compartment_name, 1)

    species = model.createMetabolite(name, compartment_name, initial_concentration)
    if species is None:
        raise ValueError('A species named ' + name + ' already exists in compartment ' + compartment_name)

    _set_species(species, model, **kwargs)

    return species


def add_parameter(name, initial_value=1.0, **kwargs):
    """Adds a new global parameter to the model.

        :param name: the name for the new global parameter
        :type name: str

        :param initial_value: optional the initial value of the parameter (defaults to 1)
        :type initial_value: float

        :param kwargs: optional parameters, recognized are:

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

            * all other parameters from :func:`set_parameters`.

        :return: the newly created parameter
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    parameter = model.createModelValue(name, initial_value)
    if parameter is None:
        raise ValueError('A global parameter named ' + name + ' already exists')

    if len(kwargs) and parameter:
        _set_parameter(parameter, model, **kwargs)

    return parameter


def add_event(name, trigger, assignments, **kwargs):
    """Adds a new event to the model.

        :param name: the name for the new event
        :type name: str

        :param trigger: the trigger expression to be used. The expression can consist of all display names.
               for example `Time > 10` would make the event trigger at time 10.
        :type trigger: str

        :param assignments: All the assignments that should be made, when the event fires. This should be a
                list of tuples where the first element is the name of the element to change, and the second element
                the assignment expression.
        :type assignments: [(str,str)]

        :param kwargs: optional parameters, recognized are:

             * | `model`: to specify the data model to be used (if not specified
               | the one from :func:`.get_current_model` will be taken)
             * 'new_name': the new name for the event
             * 'delay': the delay expression
             * 'priority': the priority expression
             * 'persistent': boolean indicating if the event is persistent
             * 'delay_calculation': boolean indicating whether just the assignment is delayed, or the
                                    calculation as well
             * 'fire_at_initial_time': boolean indicating if the event should fire at the initial time

        :return: the newly created event
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    event = model.createEvent(name)
    if event is None:
        raise ValueError('An Event named ' + name + ' already exists')
    assert (isinstance(event, COPASI.CEvent))

    _set_event(event, dm, trigger=trigger, assignments=assignments, **kwargs)

    return event


def set_event(name, exact=False, trigger=None, assignments=None, **kwargs):
    """Sets properties of the named event

    :param name: the name of the event (or a substring of the name)
    :type name: str

    :param exact: boolean indicating, that the name has to be exact
    :type exact: bool

    :param trigger: the trigger expression to be used. The expression can consist of all display names.
               for example `Time > 10` would make the event trigger at time 10.
    :type trigger: str or None

    :param assignments: All the assignments that should be made, when the event fires. This should be a
                list of tuples where the first element is the name of the element to change, and the second element
                the assignment expression.
    :type assignments: [(str,str)] or None


    :param kwargs: optional parameters, recognized are:

         * | `model`: to specify the data model to be used (if not specified
           | the one from :func:`.get_current_model` will be taken)
         * 'new_name': the new name for the event
         * 'delay': the delay expression
         * 'priority': the priority expression
         * 'persistent': boolean indicating if the event is persistent
         * 'delay_calculation': boolean indicating whether just the assignment is delayed, or the calculation as well
         * 'fire_at_initial_time': boolean indicating if the event should fire at the initial time

    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    events = model.getEvents()
    num_events = events.size()

    for i in range(num_events):
        event = events.get(i)
        assert (isinstance(event, COPASI.CEvent))

        current_name = event.getObjectName()
        display_name = event.getObjectDisplayName()

        if name and type(name) is str and exact and name != current_name and name != display_name:
            continue

        if 'name' in kwargs and kwargs['name'] not in current_name and kwargs['name'] not in display_name:
            continue

        if name and type(name) is str and name not in current_name and name not in display_name:
            continue

        _set_event(event, dm, assignments, trigger, **kwargs)


def _set_event(event, dm, assignments, trigger, **kwargs):
    """Sets the event attributes

    :param event: the event to change
    :type event: COPASI.CEvent
    :param dm: the datamodel
    :type dm: COPASI.CDataModel
    :param assignments: dictionary of event assignments
    :param trigger: trigger expression
    :param kwargs: other attributes
       - 'new_name': the new name for the event
       - 'delay': the delay expression
       - 'priority': the priority expression
       - 'persistent': boolean indicating if the event is persistent
       - 'delay_calculation': boolean indicating whether just the assignment is delayed, or the calculation as well
       - 'fire_at_initial_time': boolean indicating if the event should fire at the initial time
       - 'model': the model to use
    :return:
    """

    if not event or not dm:
        return

    need_compile = False
    if 'new_name' in kwargs and not event.setObjectName(kwargs['new_name']):
        logger.warning('could not rename event')

    if 'delay' in kwargs:
        event.setDelayExpression(_replace_names_with_cns(kwargs['delay'], model=dm))
        need_compile = True

    if 'priority' in kwargs:
        event.setPriorityExpression(_replace_names_with_cns(kwargs['priority'], model=dm))
        need_compile = True

    if 'persistent' in kwargs:
        event.setPersistentTrigger(bool(kwargs['persistent']))
        need_compile = True

    if 'delay_calculation' in kwargs:
        event.setDelayAssignment(bool(kwargs['delay_calculation']))
        need_compile = True

    if 'fire_at_initial_time' in kwargs:
        event.setFireAtInitialTime(bool(kwargs['fire_at_initial_time']))
        need_compile = True

    if trigger:
        event.setTriggerExpression(_replace_names_with_cns(trigger, model=dm))
        need_compile = True

    if assignments:
        for assignment in assignments:
            target = dm.findObjectByDisplayName(assignment[0])
            if target is None:
                logger.warning("Couldn't resolve target for event assignment {0}, skipping.".format(assignment[0]))
                continue
            if target.getObjectType() == 'Reference':
                target = target.getObjectParent()
            ea = event.createAssignment(target.getCN())
            assert (isinstance(ea, COPASI.CEventAssignment))
            ea.setExpression(_replace_names_with_cns(assignment[1], model=dm))

        need_compile = True

    if need_compile:
        model_list = COPASI.ContainerList()
        model_list.push_back(dm)
        event.compile(model_list)

    dm.getModel().compileIfNecessary()


def add_event_assignment(name, assignment, exact=False, **kwargs):
    """Adds an event assignment to the named event

    :param name: the name (or substring of name) of an event
    :type name: str

    :param assignment: tuple or list of tuples of event assignments of form (target, expression)
    :type assignment: [(str,str)] or (str, str)

    :param exact: boolean indicating whether the named expression has to be exact
    :type exact: bool

    :return: None
    """
    assignments = assignment
    if not isinstance(assignments, list):
        assignments = [assignment]

    set_event(name, exact, assignments=assignments)


def add_reaction(name, scheme, **kwargs):
    """Adds a new reaction to the model

        :param name: the name for the new reaction
        :type name: str

        :param scheme: the reaction scheme for the new reaction, if it includes Species that do not exist yet
               in the model they will be created. So for example a scheme of `A -> B` would create species `A`
               and `B` if they would not exist in the model, before creating the irreversible reaction.
        :type scheme: str

        :param kwargs: optional parameters, recognized are:

           * | `model`: to specify the data model to be used (if not specified
             | the one from :func:`.get_current_model` will be taken)

           * all other parameters from :func:`set_reaction`.

        :return: the newly created reaction
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    reaction = model.createReaction(name)
    if reaction is None:
        raise ValueError('A reaction named ' + name + ' already exists')

    assert (isinstance(reaction, COPASI.CReaction))
    _set_reaction(reaction, dm, scheme=scheme, **kwargs)

    return reaction


def get_compartments(name=None, exact=False, **kwargs):
    """Returns all information about the compartments as pandas dataframe.

        :param name: optional filter expression for the compartment, if it is not included in the name,
                     the compartment will not be added to the data set.
        :type name: str

        :param exact: boolean indicating, that the name has to be exact
        :type exact: bool

        :param kwargs: optional arguments:

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

        :return: a pandas dataframe with the information about the compartment
        :rtype: pandas.DataFrame
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    compartments = model.getCompartments()
    assert (isinstance(compartments, COPASI.CompartmentVectorNS))

    num_compartments = compartments.size()
    data = []

    for i in range(num_compartments):
        compartment = compartments.get(i)
        assert (isinstance(compartment, COPASI.CCompartment))

        unit = compartment.getUnitExpression()
        if not unit:
            unit = model.getVolumeUnit()

        comp_data = {
            'name': compartment.getObjectName(),
            'type': __status_to_string(compartment.getStatus()),
            'unit': unit,
            'initial_size': compartment.getInitialValue(),
            'initial_expression': _replace_cns_with_names(compartment.getInitialExpression(), model=dm),
            'dimensionality': compartment.getDimensionality(),
            'expression': _replace_cns_with_names(compartment.getExpression(), model=dm),
            'size': compartment.getValue(),
            'rate': compartment.getRate(),
            'key': compartment.getKey(),
            'sbml_id': compartment.getSBMLId(),
            'display_name': compartment.getObjectDisplayName(),
        }

        if name and exact and name != comp_data['name']:
            continue

        if 'name' in kwargs and kwargs['name'] not in comp_data['name']:
            continue

        if name and name not in comp_data['name']:
            continue

        if 'type' in kwargs and kwargs['type'] not in comp_data['type']:
            continue

        if 'sbml_id' in kwargs and kwargs['sbml_id'] != comp_data['sbml_id']:
            continue

        data.append(comp_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_parameters(name=None, exact=False, **kwargs):
    """Returns all information about the global parameters as pandas dataframe.

        :param name: optional filter expression for the parameters, if it is not included in the name,
                     the parameter will not be added to the data set.
        :type name: str

        :param exact: boolean indicating that the name has to be exact
        :type exact: bool

        :param kwargs: optional arguments:

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

        :return: a pandas dataframe with the information about the parameter
        :rtype: pandas.DataFrame
        """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    parameters = model.getModelValues()
    assert (isinstance(parameters, COPASI.ModelValueVectorN))

    num_params = parameters.size()
    data = []

    for i in range(num_params):
        param = parameters.get(i)
        assert (isinstance(param, COPASI.CModelValue))

        unit = param.getUnitExpression()
        if 'unit' in kwargs and not kwargs['unit'] in unit:
            continue

        param_data = {
            'name': param.getObjectName(),
            'type': __status_to_string(param.getStatus()),
            'unit': unit,
            'initial_value': param.getInitialValue(),
            'initial_expression': _replace_cns_with_names(param.getInitialExpression(), model=dm),
            'expression': _replace_cns_with_names(param.getExpression(), model=dm),
            'value': param.getValue(),
            'rate': param.getRate(),
            'key': param.getKey(),
            'sbml_id': param.getSBMLId(),
            'display_name': param.getObjectDisplayName(),
        }

        display_name = param.getObjectDisplayName()

        if name and exact and name != param_data['name'] and name != display_name:
            continue

        if 'name' in kwargs and (kwargs['name'] not in param_data['name'] and kwargs['name'] != display_name):
            continue

        if name and (name not in param_data['name'] and name != display_name):
            continue

        if 'type' in kwargs and kwargs['type'] not in param_data['type']:
            continue

        if 'sbml_id' in kwargs and kwargs['sbml_id'] != param_data['sbml_id']:
            continue

        data.append(param_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_functions(name=None, **kwargs):
    """Returns all available functions as pandas dataframe.

           :param name: optional filter expression for the functions, if it is not included in the name,
                        the function will not be added to the data set.
           :type name: str
           :param kwargs: optional arguments:

            * `reversible`: to further filter for functions that are only reversible

            * | `suitable_for`: an optional reaction for which to filter the function list. Only functions
              | suitable for the reaction will be returned

            * | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

           :return: a pandas dataframe with the information about the functions
           :rtype: pandas.DataFrame
           """
    root = COPASI.CRootContainer.getRoot()
    assert (isinstance(root, COPASI.CRootContainer))
    functions = root.getFunctionList().loadedFunctions()
    data = []

    suitable_for = None
    num_substrates = None
    num_products = None
    reversibility = False

    if 'suitable_for' in kwargs:
        dm = model_io.get_model_from_dict_or_default(kwargs)
        suitable_for = dm.getModel().getReaction(kwargs['suitable_for'])
        if suitable_for is None:
            logger.error('No reaction {0} found'.format(kwargs['suitable_for']))
            return None
        assert (isinstance(suitable_for, COPASI.CReaction))
        eqn = suitable_for.getChemEq()
        num_substrates = eqn.getSubstrates().size()
        num_products = eqn.getProducts().size()
        reversibility = suitable_for.isReversible()
        functions = COPASI.CRootContainer.getFunctionList().suitableFunctions(
            num_substrates, num_products, reversibility)

    for index in range(functions.size()):
        try:
            kin_function = functions.get(index)
        except AttributeError:
            kin_function = functions[index]

        assert (isinstance(kin_function, COPASI.CFunction))

        fun_data = {
            'name': kin_function.getObjectName(),
            'reversible': kin_function.isReversible() == 1,
            'formula': kin_function.getInfix(),
            'general': kin_function.isReversible() == -1,
        }

        if 'name' in kwargs and kwargs['name'] not in fun_data['name']:
            continue

        if 'reversible' in kwargs and kwargs['reversible'] != fun_data['reversible']:
            continue

        if name and name not in fun_data['name']:
            continue

        fun_data['mapping'] = _get_function_mapping(kin_function)

        data.append(fun_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def _get_function(name):
    """ Resolves the function from name

    :param name: name of a function in the function database
    :type name: str
    :return: the function or None
    :rtype: COPASI.CFunction
    """
    root = COPASI.CRootContainer.getRoot()
    assert (isinstance(root, COPASI.CRootContainer))
    functions = root.getFunctionList().loadedFunctions()

    for i in range(functions.size()):
        fun = functions.get(i)
        if fun.getObjectName() == name:
            return fun

    return None


def _get_function_mapping(kin_function, filter=None):
    """ Returns the mapping for the given function

    :param kin_function: the function to get the mapping for
    :type kin_function: COPASI.CFunction
    :param filter: usage to filter for (e.g. `modifier`)
    :type filter: str
    :return: the mapping as dictionary
    :rtype: Dict
    """
    mapping = {}
    if kin_function is None:
        return mapping

    params = kin_function.getVariables()
    assert (isinstance(params, COPASI.CFunctionParameters))
    for i in range(params.size()):
        p = params.getParameter(i)
        assert (isinstance(p, COPASI.CFunctionParameter))
        n = p.getObjectName()
        u = __usage_to_string(p.getUsage())
        if filter and filter != u:
            continue
        mapping[n] = u

    return mapping


def get_reaction_parameters(name=None, **kwargs):
    """Returns all local parameters as pandas dataframe.

       This also includes global parameters that are mapped to local ones.

       :param name: optional filter expression, if it is not included in the name,
                    the function will not be added to the data set.
       :type name: str

       :param kwargs: optional arguments:

        * | `reaction_name`: to further filter for local parameters of only certain reactions
          | (that contain the substring)

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

       :return: a pandas dataframe with the information about local parameters
       :rtype: pandas.DataFrame
       """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    reactions = model.getReactions()

    num_reactions = reactions.size()
    data = []

    for i in range(num_reactions):
        reaction = reactions.get(i)

        if 'reaction_name' in kwargs and kwargs['reaction_name'] != reaction.getObjectName():
            continue

        parameter_group = reaction.getParameters()
        fun_params = reaction.getFunctionParameters()
        num_params = fun_params.size()
        param_objects = reaction.getParameterObjects()
        info = COPASI.CReactionInterface()
        info.init(reaction)

        for j in range(num_params):
            fun_parameter = fun_params.getParameter(j)
            if fun_parameter.getUsage() != COPASI.CFunctionParameter.Role_PARAMETER:
                continue
            parameter = parameter_group.getParameter(fun_parameter.getObjectName())
            if parameter is None:
                continue

            current_param = param_objects[j][0] if param_objects[j] else None
            cn = current_param.getCN() if current_param else None
            mv = dm.getObject(current_param.getCN()) if cn else None
            is_global = not info.isLocalValue(j)
            if is_global and mv and isinstance(mv, COPASI.CModelValue):
                param_type = 'global'
                mapped_to = mv.getObjectName()
                value = mv.getInitialValue()
            else:
                param_type = 'local'
                mapped_to = ''
                value = reaction.getParameterValue(parameter.getObjectName())

            param_data = {
                'name': parameter.getObjectDisplayName(),
                'value': value,
                'reaction': reaction.getObjectName(),
                'type': param_type,
                'mapped_to': mapped_to
            }

            if 'name' in kwargs and kwargs['name'] not in param_data['name']:
                continue

            if name and name not in param_data['name']:
                continue

            if 'type' in kwargs and kwargs['type'] not in param_data['type']:
                continue

            data.append(param_data)

    if not data:
        return pandas.DataFrame()

    return pandas.DataFrame(data=data).set_index('name')


def get_reactions(name=None, exact=False, **kwargs):
    """Returns all reactions as pandas dataframe.

       :param name: optional filter expression, if it is not included in the name,
                    the reaction will not be added to the data set.
       :type name: str

       :param exact: boolean indicating, that the name has to be exact
       :type exact: bool

       :param kwargs: optional arguments:

        * | `reaction_name`: to further filter for local parameters of only certain reactions
          | (that contain the substring)

        * | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

       :return: a pandas dataframe with the information about local parameters
       :rtype: pandas.DataFrame
       """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    reactions = model.getReactions()

    num_reactions = reactions.size()
    data = []

    for i in range(num_reactions):
        reaction = reactions.get(i)
        assert (isinstance(reaction, COPASI.CReaction))

        reaction_data = {
            'name': reaction.getObjectName(),
            'scheme': reaction.getReactionScheme(),
            'flux': reaction.getFlux(),
            'particle_flux': reaction.getParticleFlux(),
            'function': reaction.getFunction().getObjectName(),
            'key': reaction.getKey(),
            'sbml_id': reaction.getSBMLId(),
            'display_name': reaction.getObjectDisplayName(),
        }

        if name and exact and name != reaction_data['name']:
            continue

        if 'name' in kwargs and kwargs['name'] not in reaction_data['name']:
            continue

        if name and name not in reaction_data['name']:
            continue

        if 'sbml_id' in kwargs and kwargs['sbml_id'] != reaction_data['sbml_id']:
            continue

        # add reaction mapping
        reaction_data['mapping'] = get_reaction_mapping(reaction, model=dm)

        data.append(reaction_data)

    if not data:
        return None

    return pandas.DataFrame(data=data).set_index('name')


def get_time_unit(**kwargs):
    """Returns the time unit of the model"""
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    time = model.getTimeUnit()

    return time


def set_compartment(name=None, exact=False, **kwargs):
    """Sets properties of the named compartment

    :param name: the name of the compartment (or a substring of the name)
    :type name: str

    :param exact: boolean indicating whether the name has to be exact
    :type exact: bool

    :param kwargs: optional arguments


        - | `new_name`: the new name for the compartment

        - | `initial_value` or `initial_size`: to set the initial size of the compartment

        - | `value` or `size`: to set the transient size of the compartment

        - | `initial_expression`: the initial expression for the compartment

        - | `status` or `type`: the type of the compartment one of `fixed`, `assignment` or `ode`

        - | `expression`: the expression for the compartment (only valid when type is `ode` or `assignment`)

        - | `dimensionality`: sets the dimensionality of the compartment (int value 1..3)

        - | `notes`: sets notes for the compartment (either plain text, or valid xhtml)

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    compartments = model.getCompartments()

    num_compartments = compartments.size()

    for i in range(num_compartments):
        compartment = compartments.get(i)
        assert (isinstance(compartment, COPASI.CCompartment))
        current_name = compartment.getObjectName()

        if name and type(name) is str and exact and name != current_name:
            continue

        if 'name' in kwargs and kwargs['name'] not in current_name:
            continue

        if name and type(name) is str and name not in current_name:
            continue

        if name and isinstance(name, Iterable) and current_name not in name:
            continue

        _set_compartment(compartment, model, **kwargs)


def _set_compartment(compartment, c_model, **kwargs):
    """Changes all compartment properties

    :param compartment: the compartment object to change
    :type compartment: COPASI.CCompartment
    :param c_model: the copasi model
    :type c_model: COPASI.CModel
    :param kwargs: all attributes to change
    :return: None
    """
    if not compartment or not c_model:
        return

    if 'new_name' in kwargs:
        compartment.setObjectName(kwargs['new_name'])
    for initial in ['initial_value', 'initial_size']:
        if initial in kwargs:
            c_model.updateInitialValues(compartment.getInitialValueReference())
            compartment.setInitialValue(float(kwargs[initial]))
            c_model.updateInitialValues(compartment.getInitialValueReference())
    for transient in ['value', 'size']:
        if transient in kwargs:
            compartment.setValue(float(kwargs[transient]))
    if 'initial_expression' in kwargs:
        _set_initial_expression(compartment, kwargs['initial_expression'], model=c_model.getObjectDataModel())
        c_model.setCompileFlag(True)
    if 'status' in kwargs:
        compartment.setStatus(__status_to_int(kwargs['status']))
    if 'type' in kwargs:
        compartment.setStatus(__status_to_int(kwargs['type']))
    if 'expression' in kwargs:
        _set_expression(compartment, kwargs['expression'], model=c_model.getObjectDataModel())
        c_model.setCompileFlag(True)
    if 'dimensionality' in kwargs:
        compartment.setDimensionality(kwargs['dimensionality'])
    if 'notes' in kwargs:
        compartment.setNotes(kwargs['notes'])
    if 'sbml_id' in kwargs:
        compartment.setSBMLId(kwargs['sbml_id'])


def _set_initial_expression(element, expression, model=None):
    """Utility function to safely set an initial expression

        :param element: model element
        :type element: COPASI.CModelEntity        

        :param expression: infix expression to set
        :param model: the data model to use
        :return: None
        """
    if element is None:
        return

    _set_safe(element.setInitialExpression, expression, model)


def _set_expression(element, expression, model=None):
    """Utility function to safely set an ODE / assignment expression

    :param element: model element
    :type element: COPASI.CModelEntity

    :param expression: infix expression to set
    :param model: the data model to use
    :return: None
    """
    if element is None:
        return
    _set_safe(element.setExpression, expression, model=model)


def _set_safe(fun, expression, model=None):
    """Calls the given function that is supposed to return a COPASI.CIssue

    :param fun: function to call
    :param expression: infic expression
    :param model: the data model to use
    :return:
    """
    result = fun(_replace_names_with_cns(expression, model=model))
    if result.isError():
        logger.error('Invalid expression: {0}'.format(expression))
        # set to empty string avoid crash
        fun('')


def set_parameters(name=None, exact=False, **kwargs):
    """Sets properties of the named parameter(s).

    :param name: the name of the parameter (or a substring of the name)
    :type name: str

    :param exact: boolean indicating whether the name has to be exact or not
    :type exact: bool

    :param kwargs: optional arguments

        - | `new_name`: the new name for the parameter
        - | `unit`: the unit expression to be set
        - | `initial_value`: to set the initial value for the parameter
        - | `value`: set the transient value for the parameter
        - | `initial_expression`: the initial expression
        - | `status` or `type`: the type of the parameter one of `fixed`, `assignment` or `ode`
        - | `expression`: the expression for the parameter (only valid when type is `ode` or `assignment`)
        - | `notes`: sets notes for the parameter (either plain text, or valid xhtml)
        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    parameters = model.getModelValues()
    assert (isinstance(parameters, COPASI.ModelValueVectorN))

    num_params = parameters.size()

    for i in range(num_params):
        param = parameters.get(i)
        assert (isinstance(param, COPASI.CModelValue))
        current_name = param.getObjectName()
        display_name = param.getObjectDisplayName()

        if name and type(name) is str and exact and name != current_name and name != display_name:
            continue

        if 'name' in kwargs and (kwargs['name'] not in current_name and kwargs['name'] != display_name):
            continue

        if name and type(name) is str and (name not in current_name and name != display_name):
            continue

        if name and isinstance(name, Iterable) and (current_name not in name and display_name not in name):
            continue

        _set_parameter(param, model, **kwargs)


def _set_parameter(param, c_model, **kwargs):
    """Changes the parameter attributes

    :param param: the parameter to change
    :type param: COPASI.CModelValue
    :param c_model: the model to change
    :type c_model: COPASI.CModel
    :param kwargs: the attributes to change
    :return:
    """
    if not param or not c_model:
        return

    if 'new_name' in kwargs and not param.setObjectName(str(kwargs['new_name'])):
        logger.warning('could not rename the parameter')

    if 'unit' in kwargs:
        param.setUnitExpression(kwargs['unit'])

    if 'initial_value' in kwargs:
        param.setInitialValue(float(kwargs['initial_value']))
        c_model.updateInitialValues(param.getInitialValueReference())

    if 'value' in kwargs:
        param.setValue(float(kwargs['value']))

    if 'initial_expression' in kwargs:
        _set_initial_expression(param, kwargs['initial_expression'], model=c_model.getObjectDataModel())
        c_model.setCompileFlag(True)

    if 'status' in kwargs:
        param.setStatus(__status_to_int(kwargs['status']))
        c_model.setCompileFlag(True)

    if 'type' in kwargs:
        param.setStatus(__status_to_int(kwargs['type']))
        c_model.setCompileFlag(True)

    if 'expression' in kwargs:
        _set_expression(param, kwargs['expression'], model=c_model.getObjectDataModel())
        c_model.setCompileFlag(True)

    if 'notes' in kwargs:
        param.setNotes(kwargs['notes'])

    if 'sbml_id' in kwargs:
        param.setSBMLId(kwargs['sbml_id'])


def set_reaction_parameters(name=None, **kwargs):
    """Sets local parameter values.

    :param name: the name of the parameter (or a substring of the name)
    :type name: str

    :param kwargs: optional arguments

        - | `reaction_name`: if specified only parameters of the named reaction will be changed
        - | `value`: the new value of the parameter to set. (only one of `value` / `mapped_to` should be defined)
        - | `mapped_to`: the name of a global parameter to map the local parameter to
        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    reactions = model.getReactions()
    num_reactions = reactions.size()

    for i in range(num_reactions):
        reaction = reactions.get(i)

        if 'reaction_name' in kwargs and kwargs['reaction_name'] != reaction.getObjectName():
            continue

        changed = False

        parameter_group = reaction.getParameters()
        fun_params = reaction.getFunctionParameters()
        num_params = fun_params.size()
        param_objects = reaction.getParameterObjects()

        for j in range(num_params):
            fun_parameter = fun_params.getParameter(j)
            if fun_parameter.getUsage() != COPASI.CFunctionParameter.Role_PARAMETER:
                continue
            param = parameter_group.getParameter(fun_parameter.getObjectName())
            if param is None:
                continue
            current_param = param_objects[j][0] if param_objects[j] else None
            cn = current_param.getCN() if current_param else None
            mv = dm.getObject(current_param.getCN()) if cn else None

            current_name = param.getObjectDisplayName()

            if 'name' in kwargs and kwargs['name'] not in current_name:
                continue

            if name and type(name) is str and name not in current_name:
                continue

            if name and isinstance(name, Iterable) and current_name not in name:
                continue

            if 'value' in kwargs:
                if mv and isinstance(mv, COPASI.CModelValue):
                    mv.setInitialValue(kwargs['value'])
                    model.updateInitialValues(mv)
                else:
                    param.setDblValue(kwargs['value'])
                    model.updateInitialValues(param)

                changed = True

            if 'mapped_to' in kwargs and current_param is not None:
                mv = model.getModelValue(kwargs['mapped_to'])
                if not mv:
                    logger.warning('No such parameter "{0}" to map to.'.format(kwargs['mapped_to']))
                    continue

                info = COPASI.CReactionInterface()
                info.init(reaction)
                for k in range(info.size()):
                    p_type = info.getUsage(k)
                    p_name = info.getParameterName(k)
                    if p_name == current_param.getObjectName() and p_type == COPASI.CFunctionParameter.Role_PARAMETER:
                        info.setMapping(k, mv.getObjectName())
                        info.writeBackToReaction(reaction)
                        fun_params = reaction.getFunctionParameters()
                        param_objects = reaction.getParameterObjects()
                        changed = True

        if changed:
            reaction.compile()
            model.setCompileFlag(True)


def set_reaction(name=None, exact=False, **kwargs):
    """Sets attributes of the named reaction.

    :param name: the name of the reaction (or a substring of the name)
    :type name: str

    :param exact: boolean indicating whether the name has to be exact
    :type exact: bool

    :param kwargs: optional arguments

        - | `new_name`: new name of the reaction

        - | `scheme`: the reaction scheme, new species will be created automatically

        - | `function`: the function from the function database to set

        - | `mapping`: an optional dictionary that maps model elements to the function
          |            parameters. (can be any volume, species, modelvalue or in case of
          |            local parameters a value)

        - | `notes`: sets notes for the reaction (either plain text, or valid xhtml)

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """

    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    reactions = model.getReactions()
    num_reactions = reactions.size()

    changed = False

    for i in range(num_reactions):
        reaction = reactions.get(i)
        assert (isinstance(reaction, COPASI.CReaction))

        current_name = reaction.getObjectName()

        if name and type(name) is str and exact and name != current_name:
            continue

        if 'name' in kwargs and kwargs['name'] not in current_name:
            continue

        if name and type(name) is str and name not in current_name:
            continue

        if name and isinstance(name, Iterable) and current_name not in name:
            continue

        changed = _set_reaction(reaction, dm, **kwargs)

    if changed:
        model.setCompileFlag(True)


def _set_reaction(reaction, dm, **kwargs):
    """Changes all reaction properties

    :param reaction: the reaction to modify
    :type reaction: COPASI.CReaction
    :param dm: the datamodel
    :type dm: COPASI.CDataModel
    :param kwargs: the attributes to change
    :return: boolean indicating, whether compilation is necessary
    """
    changed = False

    c_model = reaction.getObjectDataModel().getModel()

    if 'new_name' in kwargs:
        reaction.setObjectName(kwargs['new_name'])

    if 'scheme' in kwargs:
        info = COPASI.CReactionInterface()
        info.init(reaction)
        info.setChemEqString(kwargs['scheme'], '')
        info.createMetabolites()
        info.createOtherObjects()
        info.writeBackToReaction(reaction)
        reaction.compile()
        changed = True

    if 'function' in kwargs:
        info = COPASI.CReactionInterface()
        info.init(reaction)
        info.setFunctionAndDoMapping(kwargs['function'])

        missing = _get_parameter_mapping(info, missing_only=True)

        if missing and 'mapping' in kwargs:
            # try and apply mapping directly if we have one
            mapping = kwargs['mapping']
            for entry in missing:
                if entry['is_vector']:
                    continue
                param_name = entry['name']
                if param_name in mapping and _validate_mapping(entry['usage'],
                                                               param_name,
                                                               mapping[param_name], c_model):
                    info.setMapping(entry['index'], mapping[param_name])

        missing = _get_parameter_mapping(info, missing_only=True)
        if missing and ('mapping' not in kwargs or
                        not _valid_with_added_modifiers(
                            reaction, info, kwargs['function'],
                            kwargs['mapping'], dm)):
            # not valid yet, try and see if it were valid when adding modifiers
            logger.error(
                'the mapping for reaction "{0}" with function "{1}" is not '
                'valid and cannot be applied. (missing mapping(s) for {2})'.format(
                    reaction.getObjectName(), kwargs['function'],
                    [entry['usage'] + ': ' + entry['name'] for entry in missing]))

        info.writeBackToReaction(reaction)
        reaction.compile()
        changed = True

    if 'mapping' in kwargs:
        changed = set_reaction_mapping(reaction, kwargs['mapping'], model=dm)

    if 'notes' in kwargs:
        reaction.setNotes(kwargs['notes'])

    if 'sbml_id' in kwargs:
        reaction.setSBMLId(kwargs['sbml_id'])

    return changed


def _get_parameter_mapping(info, missing_only=False):
    """ Returns mapping from the info object

    :param info: the reaction information
    :type info: COPASI.CReactionInterface
    :return: array of mapping entries
    """
    result = []
    for i in range(info.size()):
        name = info.getParameterName(i)
        usage = __usage_to_string(info.getUsage(i))
        is_vector = info.isVector(i)
        mapping = info.getMapping(i)

        if missing_only and mapping != 'unknown':
            continue

        mappings = [s for s in info.getMappings(i)]

        result.append({
            'name': name,
            'usage': usage,
            'is_vector': is_vector,
            'mapping': mapping,
            'mappings': mappings,
            'index': i
        })
    return result


def _validate_mapping(usage, param_name, mapped_value, c_model):
    """Validates whether the mapped value is correct for the function parameter

    :param usage: str (or int) of the usage type of the function paramater
    :param param_name: name of the parameter
    :param mapped_value: the value it should be mapped to
    :param c_model: the model to look in.
    :type c_model: COPASI.CModel
    :return: True, if the value is ok, False otherwise
    """
    if type(usage) is int:
        usage = __usage_to_string(usage)

    if usage == "volume":
        result = c_model.getCompartment(mapped_value) is not None
    elif usage == "parameter":
        result = type(mapped_value) is str and c_model.getModelValue(mapped_value) is not None
    else:
        result = c_model.getMetabolite(mapped_value) is not None

    if not result:
        logger.error('Invalid mapping provilded for {0} of type {1} (invalid value {2})'
                     .format(param_name, usage, mapped_value))

    return result


def _valid_with_added_modifiers(reaction, info, function_name, mapping, dm):
    if not reaction or not info or not dm:
        return False
    kin_function = _get_function(function_name)
    modifiers = list(_get_function_mapping(kin_function, 'modifier').keys())
    if not modifiers:
        return False

    # ensure that the modifiers are mapped
    for modifier in modifiers:
        if modifier not in mapping:
            return False

    current_scheme = reaction.getReactionScheme()
    modifier_index = current_scheme.rfind(';')

    old_modfiers = '' if modifier_index < 0 else current_scheme[modifier_index+1:].strip()
    old_scheme = current_scheme if modifier_index < 0 else current_scheme[:modifier_index].strip()

    new_modifiers = old_modfiers
    for modifier in modifiers:
        if mapping[modifier] not in new_modifiers:
            new_modifiers += ' ' + mapping[modifier]

    new_scheme = old_scheme + '; ' + new_modifiers.strip()

    # now apply and see if it works
    info.setChemEqString(new_scheme, function_name)
    if not info.isValid():
        # set back to old
        info.setChemEqString(current_scheme, function_name)
        return False

    return True


def get_reaction_mapping(reaction, **kwargs):
    """Returns the reaction mapping of the given reaction

    :param reaction: name of a reaction, or the reaction object
    :type reaction: str or COPASI.CReaction

    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the dictionary with the reaction mapping
    :rtype: {}
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    if type(reaction) is str:
        r = model.getReaction(reaction)
        if r is None:
            logger.warning('No reaction with name: {0}'.format(reaction))
            return False

        reaction = r

    result = {}

    info = COPASI.CReactionInterface()
    info.init(reaction)
    for j in range(info.size()):
        p_type = info.getUsage(j)
        p_name = info.getParameterName(j)
        if p_type == COPASI.CFunctionParameter.Role_PARAMETER and info.isLocalValue(j):
            result[p_name] = reaction.getParameterValue(p_name)
            continue

        objs = info.getMappings(j)
        obj_size = objs.size()
        if obj_size == 1:
            result[p_name] = objs[0]
            continue

        obj_list = []
        for i in range(obj_size):
            obj_list.append(objs[i])
        result[p_name] = obj_list

    return result


def set_reaction_mapping(reaction, mapping, **kwargs):
    """Sets the reaction mapping of the parameters as specified in the mapping dictionary

    :param reaction: the name of the reaction (or reaction object)
    :type reaction: str or COPASI.CReaction

    :param mapping: dictionary that maps model elements to the function
           parameters. (can be any volume, species, modelvalue or in case of
          local parameters a value)
    :type mapping: {}

    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: boolean indicating whether the reaction was changed
    :rtype: bool

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    if type(reaction) is str:
        r = model.getReaction(reaction)
        if r is None:
            logger.warning('No reaction with name: {0}'.format(reaction))
            return False

        reaction = r

    changed = False

    info = COPASI.CReactionInterface()
    info.init(reaction)
    for j in range(info.size()):
        p_type = info.getUsage(j)
        p_name = info.getParameterName(j)
        if p_name in mapping:
            mapped_to = mapping[p_name]
            if p_type == COPASI.CFunctionParameter.Role_PARAMETER:
                try:
                    value = float(mapped_to)
                    # version 4.34 and below cannot change back from global to local
                    objs = COPASI.DataObjectVector()
                    objs.push_back(reaction.getParameters().getParameter(p_name))
                    reaction.setParameterObjects(p_name, objs)
                    reaction.setParameterValue(p_name, value)
                    info.init(reaction)
                    changed = True
                except ValueError:
                    obj = model.getModelValue(mapped_to)
                    if obj is None:
                        obj = dm.findObjectByDisplayName(mapped_to)
                        if obj is not None and type(obj) != COPASI.CModelValue:
                            logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                            continue
                    if obj is None:
                        logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                        continue

                    info.setMapping(j, obj.getObjectName())
                    changed = True

            elif p_type == COPASI.CFunctionParameter.Role_SUBSTRATE or \
                    p_type == COPASI.CFunctionParameter.Role_PRODUCT or \
                    p_type == COPASI.CFunctionParameter.Role_MODIFIER:

                if not info.isVector(j):
                    obj = model.getMetabolite(mapped_to)
                    if obj is None:
                        obj = dm.findObjectByDisplayName(mapped_to)
                        if obj is not None and type(obj) != COPASI.CMetab:
                            logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                            continue
                    if obj is None:
                        logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                        continue
                    info.setMapping(j, obj.getObjectName())
                    changed = True
                    continue

                mapped_list = mapped_to
                if type(mapped_list) is str:
                    mapped_list = [mapped_to]

                info.writeBackToReaction(reaction)
                current = reaction.getParameterObjects(j)
                objs = COPASI.DataObjectVector()
                for obj in current:
                    objs.append(obj)

                current_length = len(current)
                mapped_length = len(mapped_list)

                if current_length != mapped_length:
                    logger.warning('Different length encountered when setting mapping for parameter {0}: {1} != {2}'
                                   .format(p_name, current_length, mapped_length))

                smallest = min(current_length, mapped_length)
                for i in range(smallest):
                    mapped_to = mapped_list[i]
                    obj = model.getMetabolite(mapped_to)
                    if obj is None:
                        obj = dm.findObjectByDisplayName(mapped_to)
                        if obj is not None and type(obj) != COPASI.CMetab:
                            logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                            continue
                    if obj is None:
                        logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                        continue
                    objs[i] = obj

                reaction.setParameterObjects(j, objs)
                info.init(reaction)
                changed = True

            elif p_type == COPASI.CFunctionParameter.Role_VOLUME:
                obj = model.getCompartment(mapped_to)
                if obj is None:
                    obj = dm.findObjectByDisplayName(mapped_to)
                    if obj is not None and type(obj) != COPASI.CCompartment:
                        logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                        continue
                if obj is None:
                    logger.warning("Couldn't map '{0}' to parameter '{1}'".format(mapped_to, p_name))
                    continue

                info.setMapping(j, obj.getObjectName())
                changed = True

    if not changed:
        return False

    if not info.writeBackToReaction(reaction):
        logger.error("Couldn't change the reaction")
    reaction.compile()
    return True


def remove_species(name, **kwargs):
    """Deletes the named species

    :param name: the name of a species in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    metab = model.getMetabolite(name)
    if metab is None:
        logger.warning('no such metabolite {0}'.format(name))
        return

    key = metab.getKey()
    model.compileIfNecessary()
    model.removeMetabolite(key)
    model.setCompileFlag(True)
    model.compileIfNecessary()


def remove_parameter(name, **kwargs):
    """Deletes the named global parameter

    :param name: the name of a parameter in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    mv = model.getModelValue(name)
    if mv is None:
        logger.warning('no such global parameter {0}'.format(name))
        return

    key = mv.getKey()
    model.compileIfNecessary()
    model.removeModelValue(key)
    model.setCompileFlag(True)
    model.compileIfNecessary()


def remove_compartment(name, **kwargs):
    """Deletes the named compartment (and everything included)

    :param name: the name of a compartment in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    comp = model.getCompartment(name)
    if comp is None:
        logger.warning('no such compartment {0}'.format(name))
        return

    key = comp.getKey()
    model.compileIfNecessary()
    model.removeCompartment(key)
    model.setCompileFlag(True)
    model.compileIfNecessary()


def remove_event(name, **kwargs):
    """Deletes the named event

    :param name: the name of an event in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    ev = model.getEvent(name)
    if ev is None:
        logger.warning('no such event {0}'.format(name))
        return
    key = ev.getKey()
    model.compileIfNecessary()
    model.removeEvent(key)
    model.setCompileFlag(True)
    model.compileIfNecessary()


def remove_plot(name, **kwargs):
    """Deletes the named plot

    :param name: the name of an plot in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    output_list = dm.getPlotDefinitionList()
    assert (isinstance(output_list, COPASI.COutputDefinitionVector))

    for i in range(output_list.size()):
        plot = output_list.get(i)
        if plot.getObjectName() != name:
            continue

        output_list.remove(i)
        return

    logger.warning('no such plot {0}'.format(name))


def remove_report(name, **kwargs):
    """Deletes the named report

    :param name: the name of a report in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    report_list = dm.getReportDefinitionList()
    assert (isinstance(report_list, COPASI.CReportDefinitionVector))

    for i in range(report_list.size()):
        plot = report_list.get(i)
        if plot.getObjectName() != name:
            continue

        report_list.remove(i)
        return

    logger.warning('no such report {0}'.format(name))


def remove_reaction(name, **kwargs):
    """Deletes the named reaction

    :param name: the name of a reaction in the model
    :type name: str
    :param kwargs: optional arguments

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    reaction = model.getReaction(name)
    if reaction is None:
        logger.warning('no such reaction {0}'.format(name))
        return

    key = reaction.getKey()
    model.compileIfNecessary()
    model.removeReaction(key)
    model.setCompileFlag(True)
    model.compileIfNecessary()


def set_species(name=None, exact=False, **kwargs):
    """Sets properties of the named species

    :param name: the name of the species (or a substring of the name)
    :type name: str

    :param exact: boolean indicating, that the name has to be exact
    :type exact: bool

    :param kwargs: optional arguments

        - | `new_name`: the new name for the species
        - | `initial_concentration`: to set the initial concentration for the species
        - | `initial_particle_number`: to set the initial particle number for the species
        - | `initial_expression`: the initial expression for the species
        - | `concentration`: the new transient concentration for the species
        - | `particle_number`: the new transient particle number for the species
        - | `status` or `type`: the type of the species one of `fixed`, `assignment` or `ode`
        - | `expression`: the expression for the species (only valid when type is `ode` or `assignment`)
        - | `notes`: sets notes for the species (either plain text, or valid xhtml)
        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    metabs = model.getMetabolites()
    num_metabs = metabs.size()

    for i in range(num_metabs):
        metab = metabs.get(i)
        assert (isinstance(metab, COPASI.CMetab))

        current_name = metab.getObjectName()
        display_name = metab.getObjectDisplayName()

        if name and type(name) is str and exact and name != current_name and name != display_name:
            continue

        if 'name' in kwargs and kwargs['name'] not in current_name and kwargs['name'] not in display_name:
            continue

        if name and type(name) is str and name not in current_name and name not in display_name:
            continue

        if name and isinstance(name, Iterable) and current_name not in name and display_name not in name:
            continue

        _set_species(metab, model, **kwargs)


def _set_species(metab, c_model, **kwargs):
    """Changes all species properties

    :param metab: the species to edit
    :type metab: COPASI.CMetab
    :param c_model: the model to update
    :type c_model: COPASI.CModel
    :param kwargs: the attributes to change
    :return:
    """
    if not metab or not c_model:
        return

    if 'new_name' in kwargs:
        metab.setObjectName(kwargs['new_name'])
    if 'unit' in kwargs:
        metab.setUnitExpression(kwargs['unit'])
    if 'initial_concentration' in kwargs:
        c_model.updateInitialValues(metab.getInitialConcentrationReference())
        metab.setInitialConcentration(float(kwargs['initial_concentration']))
        c_model.updateInitialValues(metab.getInitialConcentrationReference())
    if 'initial_particle_number' in kwargs:
        c_model.updateInitialValues(metab.getInitialValueReference())
        metab.setInitialValue(float(kwargs['initial_particle_number']))
        c_model.updateInitialValues(metab.getInitialValueReference())
    if 'concentration' in kwargs:
        metab.setConcentration(float(kwargs['concentration']))
    if 'particle_number' in kwargs:
        metab.setValue(float(kwargs['particle_number']))
    if 'initial_expression' in kwargs:
        _set_initial_expression(metab, kwargs['initial_expression'], model=c_model.getObjectDataModel())
        c_model.setCompileFlag(True)
    if 'status' in kwargs:
        metab.setStatus(__status_to_int(kwargs['status']))
    if 'type' in kwargs:
        metab.setStatus(__status_to_int(kwargs['type']))
    if 'expression' in kwargs:
        _set_expression(metab, kwargs['expression'], model=c_model.getObjectDataModel())
        c_model.setCompileFlag(True)
    if 'notes' in kwargs:
        metab.setNotes(kwargs['notes'])
    if 'sbml_id' in kwargs:
        metab.setSBMLId(kwargs['sbml_id'])


def set_time_unit(unit, **kwargs):
    """Sets the time unit of the model.

    :param unit: the time unit expression
    :type unit: str
    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    if 'unit' in kwargs:
        model.setTimeUnit(kwargs['unit'])
    else:
        model.setTimeUnit(unit)

def get_model_name(**kwargs):
    """Returns the name of the current model.

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the name of the model
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))
    return model.getObjectName()


def get_model_units(**kwargs):
    """Returns all model units as dictionary.

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: a dictionary containing the model units in the form:
        | {
        |    'time_unit': '',
        |    'quantity_unit': '',
        |    'length_unit': '',
        |    'area_unit': '',
        |    'volume_unit': '',
        | }
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    return {
        'time_unit': model.getTimeUnit(),
        'quantity_unit': model.getQuantityUnit(),
        'length_unit': model.getLengthUnit(),
        'area_unit': model.getAreaUnit(),
        'volume_unit': model.getVolumeUnit(),
    }


def set_element_name(element, new_name, **kwargs):
    """Sets the name of the element

    :param element: the element whose name to change
    :type element: COPASI.CDataObject or str

    :param new_name: the new name for the element
    :type new_name: str

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    """
    if isinstance(element, COPASI.CDataObject):
        if not element.setObjectName(new_name):
            logger.warning("couldn't change name of the element")
        return

    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if type(element) is str:
        obj = dm.findObjectByDisplayName(element)
        if obj is not None:
            if not obj.setObjectName(new_name):
                logger.warning("couldn't change name of the element")
            return

    logger.warning("couldn't change name of the element (could not find the object)")


def set_model_unit(**kwargs):
    """Sets the model units.

    :param kwargs: optional parameters

        - | `time_unit`: time unit expression

        - | `substance_unit` or `quantity_unit`: substance unit expression

        - | `length_unit`: length unit expression

        - | `area_unit`: area unit expression

        - | `volume_unit`: volume unit expression

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    if 'time_unit' in kwargs:
        model.setTimeUnit(kwargs['time_unit'])

    if 'substance_unit' in kwargs:
        model.setQuantityUnit(str(kwargs['substance_unit']))

    if 'quantity_unit' in kwargs:
        model.setQuantityUnit(str(kwargs['quantity_unit']))

    if 'length_unit' in kwargs:
        model.setLengthUnit(str(kwargs['length_unit']))

    if 'area_unit' in kwargs:
        model.setAreaUnit(str(kwargs['area_unit']))

    if 'volume_unit' in kwargs:
        model.setVolumeUnit(str(kwargs['volume_unit']))

    model.updateInitialValues(COPASI.CCore.Framework_Concentration)


def set_model_name(new_name, **kwargs):
    """Renames the model to the provided new name

    :param new_name: the new name of the model
    :type new_name: str

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    set_element_name(dm.getModel(), new_name)


def add_amount_expressions(**kwargs):
    """ Utility function that adds model values for all metabolites to the model to compute the amount

    The global parameters created will be named amount(metab_name), and so can be accessed at any time.
    Should the amount already exist, it will not be modified.

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    for metab in model.getMetabolites():
        assert (isinstance(metab, COPASI.CMetab))
        if metab.getCompartment() is None:
            logger.warning('Cannot create an amount expression for {0} as it has no compartment'.format(
                metab.getObjectDisplayName()))
            continue

        if 'ignore_fixed' in kwargs and kwargs['ignore_fixed'] \
                and metab.getStatus() == COPASI.CModelEntity.Status_FIXED:
            continue

        if 'ignore_assignment' in kwargs and kwargs['ignore_assignment'] \
                and metab.getStatus() == COPASI.CModelEntity.Status_ASSIGNMENT:
            continue

        mv_name = 'amount({0})'.format(metab.getObjectDisplayName())
        if 'use_sbml_ids' in kwargs:
            mv_name = 'amount({0})'.format(metab.getSBMLId())

        mv = model.getModelValue(mv_name)
        if mv is not None:
            continue
        add_parameter(mv_name,
                      status='assignment',
                      expression='<{0}> * <{1}>'.format(
                          metab.getConcentrationReference().getCN(),
                          metab.getCompartment().getValueReference().getCN()),
                      notes='Amount expression for {0}'.format(metab.getObjectDisplayName())
                      )


def remove_amount_expressions(**kwargs):
    """ Utility function that removes model values created using `add_amount_expressions`.

    The global parameters created will be named amount(metab_name), and so can be accessed at any time.

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    keys = []
    for mv in model.getModelValues():
        assert (isinstance(mv, COPASI.CModelValue))
        if mv.getNotes().startswith('Amount ex'):
            keys.append(mv.getKey())

    for key in keys:
        model.removeModelValue(key)
        model.compileIfNecessary()


def have_miriam_resources():
    """Utility function returning whether MIRIAM resources are avaialble

    :return: boolean indicating whether there are MIRIAM resources available or not
    :rtype: bool
    """
    try:
        miriam = COPASI.CRootContainer.getConfiguration().getRecentMIRIAMResources()
        assert (isinstance(miriam, COPASI.CMIRIAMResources))
        return miriam.getResourceList().size() > 0
    except AttributeError:
        return False


def get_miriam_resources(compact=True):
    """Retrieves the current MIRIAM resources from the configuration

    :param compact: whether to return a compact version of the resources (default: True)
    :type compact: bool

    :return: dataframe with the list of current miriam resources
    :rtype: pandas.DataFrame
    """
    resources = []
    try:
        miriam = COPASI.CRootContainer.getConfiguration().getRecentMIRIAMResources()
        assert (isinstance(miriam, COPASI.CMIRIAMResources))
        for i in range(miriam.getResourceList().size()):
            res = miriam.getMIRIAMResource(i)
            assert (isinstance(res, COPASI.CMIRIAMResource))
            resources.append({
                'resource': res.getMIRIAMDisplayName(),
                'is_citation': res.getMIRIAMCitation(),
                'uri': res.getIdentifiersOrgURL(compact)
            })
    except AttributeError:
        logger.error("Couldn't retrieve list of miriam resources, please update the python-copasi version")

    df = pandas.DataFrame(data=resources)
    if resources:
        df = df.set_index('resource')
    return df


def update_miriam_resources():
    """This method downloads the latest miriam resources from the COPASI website and stores the configuration"""
    try:
        import biomodels
    except ImportError:
        from . import biomodels

    temp_name = tempfile.mktemp()

    with open(temp_name, mode='w', encoding='utf-8') as temp_file:
        if sys.version_info[0] < 3:
            temp_file.write(biomodels.download_from(MIRIAM_XML).decode('utf-8'))
        else:
            temp_file.write(biomodels.download_from(MIRIAM_XML))
        temp_file.close()
    config = COPASI.CRootContainer.getConfiguration()
    miriam = config.getRecentMIRIAMResources()
    assert (isinstance(miriam, COPASI.CMIRIAMResources))
    miriam.updateMIRIAMResourcesFromFile(None, temp_name)
    config.save()


def _is_number(x):
    try:
        float(x)
        return True
    except ValueError:
        return False


def _tokenize_eqn(eqn):
    """ Utility function for tokenizing equations into variables and functions

    :param eqn: an equation
    :type eqn: str
    :return: a dictionary of the tokens
    :rtype: dict
    """
    result = {}
    num_chars = len(eqn)
    i = 0

    species = []
    parameters = []
    tokens = []

    operators = ['/', '*', '+', '-', '^', '(', ')']
    functions = ['exp', 'pow', 'abs', 'floor', 'factorial',
                 'log', 'log10', 'sin', 'cos', 'tan', 'sec',
                 'csc', 'cot', 'sinh', 'cosh', 'tanh', 'sech',
                 'csch', 'coth', 'asin', 'acos', 'atan', 'arcsec',
                 'arccsc', 'arccot', 'arcsinh', 'arccosh',
                 'arctanh', 'arcsech', 'arccsch', 'arccoth',
                 'uniform', 'normal', 'gamma', 'poisson',
                 'le', 'lt', 'ge', 'gt', 'ne', 'eq', 'and',
                 'or', 'xor', 'not', 'if']

    chunk = ''
    var = None
    var_is_species = False
    var_is_initial = False
    is_ode = False
    token = None

    while i < num_chars:
        c_0 = eqn[i]
        c_1 = eqn[i + 1] if i + 1 < num_chars else None
        c_2 = eqn[i + 2] if i + 2 < num_chars else None

        if ord(c_0) > 127:  # skip non-ascii characters
            logger.warning(u'Encountered invalid character {0} while tokenizing equation, skipping it.'.format(c_0))
            i = i + 1
            continue

        if c_0 == '[' and c_1 is not None:
            var, var_is_initial, var_is_species = _store_variable(parameters, species, tokens, var, var_is_initial,
                                                                  var_is_species)
            pos = eqn.find(']', i+1)
            if pos != -1:
                var = eqn[i + 1:pos]
                var_is_species = True
                i = pos + 1
                c_0 = eqn[i] if i < num_chars else None
                c_1 = eqn[i + 1] if i + 1 < num_chars else None

                if c_0 == '_' and c_1 == '0':
                    var_is_initial = True
                    i = i + 2

                continue

        elif c_0 == '/' and c_1 == 'd' and c_2 == 't':
            is_ode = True
            i += 3

            if chunk.startswith('d'):
                chunk = chunk[1:]

            # remove trailing whitespace
            while i < num_chars and eqn[i] == ' ':
                i += 1

            continue

        elif c_0 == '=':

            if var is None:
                var = chunk

            if not var:
                if token:
                    result['lhs'] = token

                    var = None
                    var_is_species = False
                    var_is_initial = False
                    chunk = ''
                    result['is_ode'] = is_ode
                    tokens.remove(token)
                    token = None
                i += 1
                continue

            token = {
                'var': var,
                'is_species': var_is_species,
                'is_initial': var_is_initial
            }

            result['lhs'] = token

            if var_is_species and var and var not in species:
                species.append(var)
            elif not var_is_species and var and var not in parameters:
                parameters.append(var)

            var = None
            var_is_species = False
            var_is_initial = False
            chunk = ''
            result['is_ode'] = is_ode
            i = i+1
            continue

        elif c_0 == ' ':
            var, var_is_initial, var_is_species = _store_variable(parameters, species, tokens, var, var_is_initial,
                                                                  var_is_species)

            if chunk != '':
                if chunk in functions or _is_number(chunk):
                    if tokens and type(tokens[-1]) is dict:
                        tokens.append('*')
                    tokens.append(chunk)
                else:
                    token = {
                        'var': chunk,
                        'is_species': False,
                        'is_initial': False
                    }
                    if chunk not in parameters:
                        parameters.append(chunk)
                    if tokens and type(tokens[-1]) is dict:
                        tokens.append('*')
                    tokens.append(token)

            chunk = ''
        elif c_0 in operators:
            var, var_is_initial, var_is_species = _store_variable(parameters, species, tokens, var, var_is_initial,
                                                                  var_is_species)

            if chunk != '':
                if chunk in functions or _is_number(chunk):
                    if tokens and type(tokens[-1]) is dict:
                        tokens.append('*')
                    tokens.append(chunk)
                else:
                    token = {
                        'var': chunk,
                        'is_species': False,
                        'is_initial': False
                    }
                    if chunk not in parameters:
                        parameters.append(chunk)
                    if tokens and type(tokens[-1]) is dict:
                        tokens.append('*')
                    tokens.append(token)

            chunk = ''

            if c_0 == '(' and tokens and (tokens[-1] == ')' or type(tokens[-1]) is dict):
                tokens.append('*')

            tokens.append(c_0)
        else:
            chunk = chunk + c_0

        i += 1

    if chunk != '':
        if chunk in functions or _is_number(chunk):
            if tokens and type(tokens[-1]) is dict:
                tokens.append('*')
            tokens.append(chunk)
        else:
            token = {
                'var': chunk,
                'is_species': False,
                'is_initial': False
            }
            if chunk not in parameters:
                parameters.append(chunk)
            if tokens and type(tokens[-1]) is dict:
                tokens.append('*')
            tokens.append(token)

    if var is not None:
        token = {
            'var': var,
            'is_species': var_is_species,
            'is_ode': is_ode,
            'is_initial': var_is_initial
        }
        if var_is_species and var not in species:
            species.append(var)
        elif not var_is_species and var not in parameters:
            parameters.append(var)

        if tokens and type(tokens[-1]) is dict:
            tokens.append('*')
        tokens.append(token)

    result['tokens'] = tokens
    result['species'] = species
    result['parameters'] = parameters

    return result


def _store_variable(parameters, species, tokens, var, var_is_initial, var_is_species):
    if var is not None and var:
        token = {
            'var': var,
            'is_species': var_is_species,
            'is_initial': var_is_initial
        }
        if var_is_species and var not in species:
            species.append(var)
        elif not var_is_species and var not in parameters:
            parameters.append(var)
        if tokens and type(tokens[-1]) is dict:
            tokens.append('*')
        tokens.append(token)
        var = None
        var_is_species = False
        var_is_initial = False
    return var, var_is_initial, var_is_species


def add_equation(eqn, time_symbol='t', **kwargs):
    """This function allows to add arbitrary equations to the current model.

    This function allows adding arbitrary ODE's / assignments to the model. Nonexisting
    model entities will be created.

    :param eqn: the equation for example of form: d[X]/dt = k1 * exp({Time})
    :type eqn: str

    :param time_symbol: optional symbol that will be used for time (defaults to t)
    :type time_symbol: str

    :param kwargs:
    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    result = _tokenize_eqn(eqn)

    # first create all non-existing species / parameters
    for species in result['species']:
        if model.getMetabolite(species) is None:
            add_species(species)

    for parameter in result['parameters']:
        if parameter == time_symbol:
            continue
        if model.getModelValue(parameter) is None:
            add_parameter(parameter)

    # now create the equation from the tokens
    expression = ''
    for token in result['tokens']:
        if type(token) is dict:
            if token['is_species']:
                element = model.getMetabolite(token['var'])
                assert (isinstance(element, COPASI.CMetab))
                obj = element.getInitialConcentrationReference().getCN() if token[
                    'is_initial'] else element.getConcentrationReference().getCN()
            elif token['var'] == time_symbol:
                obj = model.getValueReference().getCN()
            else:
                element = model.getModelValue(token['var'])
                assert (isinstance(element, COPASI.CModelValue))
                obj = element.getInitialValueReference().getCN() if token[
                    'is_initial'] else element.getValueReference().getCN()
            expression = expression + "<" + obj.getString() + ">"
        else:
            expression = expression + token

    # then lets add the equation
    if 'lhs' in result:
        lhs = result['lhs']
        if lhs['is_species']:
            element = model.getMetabolite(lhs['var'])
        else:
            element = model.getModelValue(lhs['var'])

        assert (isinstance(element, COPASI.CModelEntity))
        element.setStatus(COPASI.CModelEntity.Status_ODE if result['is_ode'] else COPASI.CModelEntity.Status_ASSIGNMENT)
        issue = element.setExpression(expression)
        assert (isinstance(issue, COPASI.CIssue))
        assert (issue.isSuccess())

    # mark the model as needing to be compiled
    model.setCompileFlag(True)


def _annotated_matrix_to_df_1d(ann_matrix):
    assert (isinstance(ann_matrix, COPASI.CDataArray))
    dim = ann_matrix.dimensionality()
    if dim != 1:
        logger.error('only one dimensional matrices are supported by this method')
        return None

    matrix = ann_matrix.getArray()
    index = [s for s in ann_matrix.getAnnotationsString(0)]
    data = []
    v = COPASI.SizeTStdVector()
    v.push_back(0)
    for x in range(len(index)):
        v[0] = x
        data.append(matrix.get(v))
    return pd.DataFrame(data, index=index)


def _annotated_matrix_to_df(ann_matrix):
    """Converts the annotated matrix to a pandas dataframe

    :param ann_matrix: the matrix to convert
    :type ann_matrix: COPASI.CDataArray
    :return: a pandas dataframe representing the matrix
    :rtype: pd.DataFrame
    """
    assert (isinstance(ann_matrix, COPASI.CDataArray))
    dim = ann_matrix.dimensionality()
    if dim != 2:
        if dim == 1:
            return _annotated_matrix_to_df_1d(ann_matrix)

        if dim == 0:
            return ann_matrix.getArray().get(COPASI.SizeTStdVector())

        logger.error('only 0-two dimensional matrices are supported at this time')
        return None

    matrix = ann_matrix.getArray()
    columns = [s for s in ann_matrix.getAnnotationsString(1)]
    index = [s for s in ann_matrix.getAnnotationsString(0)]
    data = []
    for x in range(len(index)):
        row = {}
        for y in range(len(columns)):
            row[columns[y]] = matrix.get(x, y)
        data.append(row)
    return pd.DataFrame(data, columns=columns, index=index)


def get_stoichiometry_matrix(**kwargs):
    """Returns the stoichiometry matrix of the model

    :return: the stoichiometry matrix of the current model
    :rtype: pd.DataFrame

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))
    return _annotated_matrix_to_df(model.getStoiAnnotation())


def get_reduced_stoichiometry_matrix(**kwargs):
    """Returns the reduced stoichiometry matrix of the model

    :return: the stoichiometry matrix of the current model
    :rtype: pd.DataFrame

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))
    return _annotated_matrix_to_df(model.getRedStoiAnnotation())


def get_jacobian_matrix(apply_initial_values=False, **kwargs):
    """Returns the jacobian matrix of the model at the current state

    :param apply_initial_values: if set to the the initial values will be applied, otherwise
         the jacobian from the current state will be returned
    :type apply_initial_values: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the stoichiometry matrix of the current model
    :rtype: pd.DataFrame

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    if apply_initial_values:
        model.applyInitialValues()

    jacobian = COPASI.FloatMatrix()
    model.getMathContainer().calculateJacobian(jacobian, 1e-12, False)
    state_template = model.getStateTemplate()
    user_order = state_template.getUserOrder()
    name_vector = []

    for i in range(0, user_order.size()):
        entity = state_template.getEntity(user_order.get(i))
        if entity is None:
            continue
        status = entity.getStatus()

        if status == COPASI.CModelEntity.Status_ODE or \
                (status == COPASI.CModelEntity.Status_REACTIONS and entity.isUsed()):
            name_vector.append(entity.getObjectName())

    assert len(name_vector) == jacobian.numRows()

    data = []

    for i in range(0, len(name_vector)):
        row = {}
        for j in range(0, len(name_vector)):
            row[name_vector[j]] = jacobian.get(i, j)
        data.append(row)

    return pd.DataFrame(data, columns=name_vector, index=name_vector)


def get_reduced_jacobian_matrix(apply_initial_values=False, **kwargs):
    """Returns the jacobian matrix of the reduced model at the current state

        :param apply_initial_values: if set to the the initial values will be applied, otherwise
             the jacobian from the current state will be returned
        :type apply_initial_values: bool

        :param kwargs: optional parameters

            - | `model`: to specify the data model to be used (if not specified
              | the one from :func:`.get_current_model` will be taken)

        :return: the stoichiometry matrix of the reduced current model
        :rtype: pd.DataFrame

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    if apply_initial_values:
        model.applyInitialValues()

    jacobian = COPASI.FloatMatrix()
    model.getMathContainer().calculateJacobian(jacobian, 1e-12, True)
    state_template = model.getStateTemplate()
    name_vector = []
    data = []

    i_max = state_template.getNumIndependent()

    for i in range(0, i_max):
        name_vector.append(state_template.getIndependent(i).getObjectName())

    for i in range(0, i_max):
        row = {}
        for j in range(0, i_max):
            row[name_vector[j]] = jacobian.get(i, j)
        data.append(row)

    return pd.DataFrame(data, columns=name_vector, index=name_vector)


def _get_group_as_dict(group, basic_only=True):
    """Returns the values from the given parameter group as dictionary

    :param group: the copasi parameter group
    :type group: COPASI.CCopasiParameterGroup

    :param basic_only: boolean indicating, whether only basic parameters should be returned (default)
    :type basic_only: bool

    :return: dictionary with the values
    """
    result = {}
    for i in range(group.size()):
        param = group.getParameter(i)
        assert (isinstance(param, COPASI.CCopasiParameter))

        if isinstance(param, COPASI.CCopasiParameterGroup):
            continue

        if not param.isEditable():
            continue

        if not param.isBasic() and basic_only:
            continue

        name = param.getObjectName()
        param_type = param.getType()

        if param_type == COPASI.CCopasiParameter.Type_STRING:
            result[name] = param.getStringValue()
        elif param_type == COPASI.CCopasiParameter.Type_INT:
            result[name] = param.getIntValue()
        elif param_type == COPASI.CCopasiParameter.Type_UINT:
            result[name] = param.getUIntValue()
        elif param_type == COPASI.CCopasiParameter.Type_DOUBLE:
            result[name] = param.getDblValue()
        elif param_type == COPASI.CCopasiParameter.Type_UDOUBLE:
            result[name] = param.getUDblValue()
        elif param_type == COPASI.CCopasiParameter.Type_BOOL:
            result[name] = param.getBoolValue()

    return result


def _set_group_from_dict(group, values):
    """Changes settings in the given parameter group to the values specified in the values dictionary

    :param group: the copasi parameter group to update
    :type group: COPASI.CCopasiParameterGroup
    :param values: dictionary with new values
    :type values: dict
    :return:
    """
    for key in values:
        param = group.getParameter(key)
        if param is None:
            continue

        if isinstance(param, COPASI.CCopasiParameterGroup):
            continue

        assert (isinstance(param, COPASI.CCopasiParameter))
        param_type = param.getType()

        try:
            if param_type == COPASI.CCopasiParameter.Type_STRING:
                param.setStringValue(str(values[key]))
            elif param_type == COPASI.CCopasiParameter.Type_INT:
                param.setIntValue(int(values[key]))
            elif param_type == COPASI.CCopasiParameter.Type_UINT:
                param.setUIntValue(int(values[key]))
            elif param_type == COPASI.CCopasiParameter.Type_DOUBLE:
                param.setDblValue(float(values[key]))
            elif param_type == COPASI.CCopasiParameter.Type_UDOUBLE:
                param.setUDblValue(float(values[key]))
            elif param_type == COPASI.CCopasiParameter.Type_BOOL:
                param.setBoolValue(bool(values[key]))
        except TypeError:
            logger.error('could not set value {0} for parameter {1}'.format(values[key], key))


def get_task_settings(task, basic_only=True, **kwargs):
    """Returns the settings of the given task

    :param task: the task to read the settings of
    :type task: COPASI.CCopasiTask or str

    :param basic_only: boolean flag, indicating that only the basic parameters should be returned
    :type basic_only: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: dict of task settings
    :rtype: {}

    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if not isinstance(task, COPASI.CCopasiTask):
        name = task
        task = dm.getTask(name)

    if not isinstance(task, COPASI.CCopasiTask):
        logger.error('no such task')
        return {}

    result = {
        'scheduled': task.isScheduled(),
        'update_model': task.isUpdateModel(),
    }

    problem = task.getProblem()
    result['problem'] = _get_group_as_dict(problem, basic_only)

    method = task.getMethod()
    result['method'] = _get_group_as_dict(method, basic_only)
    result['method']['name'] = method.getObjectName()

    report = task.getReport()
    assert (isinstance(report, COPASI.CReport))
    report_def = report.getReportDefinition()
    result['report'] = {
        'filename': report.getTarget(),
        'report_definition': report_def.getObjectName() if report_def is not None else None,
        'append': report.getAppend(),
        'confirm_overwrite': report.confirmOverwrite()
    }

    return result


def set_task_settings(task, settings, **kwargs):
    """Applies the task settings present in the settings object

    :param task: the task to set
    :type task: COPASI.CCopasiTask or str
    :param settings: dictionary in the same format as the ones obtained from :func:`.get_task_settings`
    :type settings: dict

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    if not isinstance(task, COPASI.CCopasiTask):
        name = task
        task = dm.getTask(name)

    if not isinstance(task, COPASI.CCopasiTask):
        logger.error('no such task')
        return

    if 'is_scheduled' in settings:
        task.setScheduled(settings['is_scheduled'])

    if 'scheduled' in settings:
        task.setScheduled(settings['scheduled'])

    if 'is_update_model' in settings:
        task.setUpdateModel(settings['is_update_model'])

    if 'update_model' in settings:
        task.setUpdateModel(settings['update_model'])

    if 'problem' in settings:
        problem = task.getProblem()
        _set_group_from_dict(problem, settings['problem'])

        if isinstance(problem, COPASI.CTrajectoryProblem):            
            if 'Duration' in settings['problem']:
                problem.setDuration(float(settings['problem']['Duration']))
            if 'StepSize' in settings['problem']:
                problem.setStepSize(float(settings['problem']['StepSize']))
            if 'intervals' in settings['problem']:
                problem.setStepNumber(int(settings['problem']['intervals']))
            if 'StepNumber' in settings['problem']:
                problem.setStepNumber(int(settings['problem']['StepNumber']))

    if 'method' in settings:
        method = task.getMethod()
        m_dict = settings['method']
        if 'name' in m_dict:
            name = m_dict['name']
            if name != method.getObjectName():
                task.setMethodType(COPASI.CCopasiMethod.TypeNameToEnum(name))
                method = task.getMethod()

        _set_group_from_dict(method, m_dict)

    if 'report' in settings:
        r_dict = settings['report']
        report = task.getReport()
        assert (isinstance(report, COPASI.CReport))
        if 'filename' in r_dict:
            report.setTarget(r_dict['filename'])

        if 'append' in r_dict:
            report.setAppend(r_dict['append'])

        if 'confirm_overwrite' in r_dict:
            report.setConfirmOverwrite(r_dict['confirm_overwrite'])

        if 'report_definition' in r_dict:
            name = r_dict['report_definition']
            if name is not None:
                r_def = dm.getReportDefinition(name)
                if r_def is not None:
                    report.setReportDefinition(r_def)


def _collect_data(names=None, cns=None, **kwargs):
    """Collects data from the model, returning it as dataframe

    :param names: list of names of elements to return
    :type names: list or None
    :param cns: list of common names of references for which to return the results
    :type cns: list or None
    :return: data frame with the results
    :rtype: pd.DataFrame
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    data = []

    value = None

    if cns:
        for cn in cns:
            c_cn = COPASI.CCommonName(cn)
            obj = model.getObject(c_cn)
            if obj is None:
                # couldn't find that object in the model
                logger.warning('No object for cn: {0}'.format(str(cn)))
                continue
            assert (isinstance(obj, COPASI.CDataObject))
            value = _get_value_from_reference(obj)

            data.append({'name': obj.getObjectDisplayName(), 'value': value})

    if names:
        for name in names:
            obj = model.findObjectByDisplayName(name)
            if obj is None:
                # couldn't find that object in the model
                logger.warning('No object for name: {0}'.format(name))
                continue
            assert (isinstance(obj, COPASI.CDataObject))
            if obj.getObjectType() == 'Reference':
                value = _get_value_from_reference(obj)
            else:
                if isinstance(obj, COPASI.CMetab):
                    value = _get_value_from_reference(obj.getConcentrationReference())
                elif isinstance(obj, COPASI.CModelValue) or \
                        isinstance(obj, COPASI.CCompartment) or isinstance(obj, COPASI.CModel):
                    value = _get_value_from_reference(obj.getValueReference())
                elif isinstance(obj, COPASI.CReaction):
                    value = _get_value_from_reference(obj.getFluxReference())
                elif isinstance(obj, COPASI.CCopasiParameter):
                    value = _get_value_from_reference(obj.getValueReference())
                else:
                    value = None

            data.append({'name': obj.getObjectDisplayName(), 'value': value})

    return pandas.DataFrame(data=data).set_index('name')


def _get_value_from_reference(obj):
    """ Utility function returning a value from the given reference object

    :param obj: the reference object
    :type obj: COPASI.CDataObject
    :return: the value, if it could be resolved or None
    :rtype: float or None
    """
    if obj is None:
        return None
    parent = obj.getObjectParent()
    if parent is None:
        return None
    name = obj.getObjectName()
    value = _get_named_value(parent, name)
    return value


def _get_named_value(obj, name):
    """ Utility function that returns the value of the given copasi object

    :param obj: a copasi object, that could be a compartment, species, parameter, reaction
    :param name: the reference name to return
    :return:
    """
    is_metab = isinstance(obj, COPASI.CMetab)
    is_reaction = isinstance(obj, COPASI.CReaction)
    is_model = isinstance(obj, COPASI.CModel)
    is_cparam = isinstance(obj, COPASI.CCopasiParameter)

    value = None

    if is_reaction:
        value = {
            'Flux': obj.getFlux(),
            'ParticleFlux': obj.getParticleFlux(),
        }.get(name, None)

    elif is_metab:
        value = {
            'ParticleNumber': obj.getValue(),
            'ParticleNumberRate': obj.getRate(),
            'InitialParticleNumber': obj.getInitialValue(),
            'InitialConcentration': obj.getInitialConcentration(),
            'Concentration': obj.getConcentration(),
            'Rate': obj.getConcentrationRate(),
        }.get(name, None)

    elif is_model:
        value = {
            'Time': obj.getValue(),
        }.get(name, None)

    elif is_cparam:
        # this will be the case if it is a local parameter
        parent = obj.getObjectParent().getObjectParent()
        assert (isinstance(parent, COPASI.CReaction))
        value = parent.getParameterValue(obj.getObjectName())

    if pd.isna(value):
        value = {
            'Time': obj.getValue(),
            'Volume': obj.getValue(),
            'Value': obj.getValue(),
            'Rate': obj.getRate(),
            'InitialValue': obj.getInitialValue(),
            'InitialVolume': obj.getInitialValue(),
            'InitialParticleNumber': obj.getInitialValue(),
        }.get(name, None)

    return value


def _set_value_from_reference(obj, new_value):
    """ Utility function setting a value from the given reference object

    :param obj: the reference object
    :type obj: COPASI.CDataObject
    :param new_value: the new value to set
    :type new_value: float
    :return: None
    """
    if obj is None:
        return None
    parent = obj.getObjectParent()
    if parent is None:
        return None
    name = obj.getObjectName()
    _set_named_value(parent, name, new_value, obj)
    return


def _set_named_value(obj, name, new_value, ref):
    """ Utility function that sets the value of the given copasi object

    :param obj: a copasi object, that could be a compartment, species, parameter, reaction
    :param name: the reference name to set
    :param new_value: the new value for the element
    :param ref: the reference object
    :return:None
    """
    is_metab = isinstance(obj, COPASI.CMetab)
    is_reaction = isinstance(obj, COPASI.CReaction)
    is_model = isinstance(obj, COPASI.CModel)
    is_cparam = isinstance(obj, COPASI.CCopasiParameter)

    p_name = None
    set_function = None

    if is_reaction:
        set_function = {
            'Flux': obj.setFlux,
            'ParticleFlux': obj.setParticleFlux,
        }.get(name, None)

    elif is_metab:
        set_function = {
            'ParticleNumber': obj.setValue,
            'InitialParticleNumber': obj.setInitialValue,
            'InitialConcentration': obj.setInitialConcentration,
            'Concentration': obj.setConcentration,
        }.get(name, None)

    elif is_model:
        set_function = {
            'Time': obj.setValue,
        }.get(name, None)

    elif is_cparam:
        # this will be the case if it is a local parameter
        parent = obj.getObjectParent().getObjectParent()
        assert (isinstance(parent, COPASI.CReaction))
        p_name = obj.getObjectName()
        set_function = parent.setParameterValue

    if pd.isna(set_function):
        set_function = {
            'Time': obj.setValue,
            'Volume': obj.setValue,
            'Value': obj.setValue,
            'InitialValue': obj.setInitialValue,
            'InitialVolume': obj.setInitialValue,
            'InitialParticleNumber': obj.setInitialValue,
        }.get(name, None)

    dm = basico.get_current_model()
    model = dm.getModel()

    if set_function is not None:
        if p_name is not None:
            set_function(p_name, new_value)
            model.updateInitialValues(ref)
        else:
            set_function(new_value)
            model.updateInitialValues(ref)


def set_value(name_or_reference, new_value, initial=False, **kwargs):
    """Gets the value of the named element or nones

    :param name_or_reference: display name of model element
    :type name_or_reference: str or COPASI.CDataObject

    :param new_value: the new value to set
    :type new_value: float

    :param initial: if True, an initial value will be set, rather than a transient one. If set to `None`, the
                    default reference will be returned and not coerced.
    :type initial: bool or None

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the value if found or None
    :rtype: float or None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    obj = _get_object(name_or_reference, initial=initial, model=model)

    if obj is None:
        return None

    return _set_value_from_reference(obj, new_value)


def _get_object(name_or_reference, initial=False, **kwargs):
    """Returns the reference object for the given name

    :param name_or_reference: display name of model element
    :type name_or_reference: str or COPASI.CDataObject

    :param initial: if True, an initial reference will be returned, rather than a transient one. If set to `None`, the
                    default reference will be returned and not coerced.
    :type initial: bool or None

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the reference object or None
    :rtype: COPASI.CDataObject or None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    if isinstance(name_or_reference, COPASI.CDataObject):
        obj = name_or_reference
    else:
        obj = model.findObjectByDisplayName(name_or_reference)
        if obj is None:
            return None
        if obj.getObjectType() == 'Metabolite':
            if initial:
                obj = obj.getInitialConcentrationReference()
            else:
                obj = obj.getConcentrationReference()
        elif obj.getObjectType() != 'Reference':
            if initial and not isinstance(obj, COPASI.CCopasiParameter):
                obj = obj.getInitialValueReference()
            else:
                obj = obj.getValueReference()

        # ensure reference is initial or transient as required
        if initial is not None:
            if initial:
                obj_name = obj.getObjectName()
                if obj_name == 'Concentration':
                    obj = obj.getObjectParent().getInitialConcentrationReference()
                if obj_name in ['Value', 'Volume', 'ParticleNumber']:
                    parent = obj.getObjectParent()
                    if not isinstance(parent, COPASI.CCopasiParameter):
                        obj = parent.getInitialValueReference()
            else:
                obj_name = obj.getObjectName()
                if obj_name == 'InitialConcentration':
                    obj = obj.getObjectParent().getConcentrationReference()
                if obj_name in ['InitialValue', 'InitialVolume', 'InitialParticleNumber']:
                    parent = obj.getObjectParent()
                    obj = parent.getValueReference()
    return obj


def get_value(name_or_reference, initial=False, **kwargs):
    """Gets the value of the named element or nones

    :param name_or_reference: display name of model element
    :type name_or_reference: str or COPASI.CDataObject

    :param initial: if True, an initial value will be returned, rather than a transient one. If set to `None`, the
                    default reference will be returned and not coerced.
    :type initial: bool or None

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the value if found or None
    :rtype: float or None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    obj = _get_object(name_or_reference, initial=initial, model=model)

    if obj is None:
        return None

    return _get_value_from_reference(obj)


def get_cn(name_or_reference, initial=False, **kwargs):
    """Gets the cn of the named element or none

    :param name_or_reference: display name of model element
    :type name_or_reference: str or COPASI.CDataObject

    :param initial: if True, an initial reference cn will be returned, rather than a transient one
    :type initial: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: the cn if found or None
    :rtype: str or None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    obj = _get_object(name_or_reference, initial=initial, model=model)

    if obj is None:
        return None

    return obj.getCN().getString()


def assign_report(name, task, filename='', append=True, confirm_overwrite=True, **kwargs):
    """Assigns the named report to the specified task

    :param name: the name of the report definition to assign
    :type name: str

    :param task: the task to assign the report to
    :type task: Union[int, str, COPASI.CCopasiTask]

    :param filename: the filename to write the result to or `''`, if it is the empty, it resets the target
           of the task, and COPASI will not create that report
    :type filename: str

    :param append: boolean indicating whether output should be appended (defaults to True)
    :type append: bool

    :param confirm_overwrite: boolean indicating whether the copasi should ask before overwriting a file
    :type confirm_overwrite: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    if isinstance(task, int) or isinstance(task, str):
        obj = model.getTask(task)
        if obj is None:
            logger.error('No task {0}'.format(task))
            return
        task = obj

    report_definition = model.getReportDefinition(name)
    if not report_definition:
        logger.error('No report definition: {0}'.format(name))

    assert (isinstance(report_definition, COPASI.CReportDefinition))
    assert (isinstance(task, COPASI.CCopasiTask))

    r = task.getReport()
    assert (isinstance(r, COPASI.CReport))
    r.setAppend(append)
    r.setConfirmOverwrite(confirm_overwrite)
    r.setTarget(filename)
    r.setReportDefinition(report_definition)


def remove_report_from_task(task, **kwargs):
    """Clears the report filename from the specified task

    :param task: the task to assign the report to
    :type task: Union[int, str, COPASI.CCopasiTask]

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)

    if isinstance(task, int) or isinstance(task, str):
        obj = model.getTask(task)
        if obj is None:
            logger.error('No task {0}'.format(task))
            return
        task = obj

    assert (isinstance(task, COPASI.CCopasiTask))
    task.getReport().setTarget('')


def get_scheduled_tasks(**kwargs):
    """Returns the list of scheduled tasks

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: list of tasks that are scheduled
    :rtype: [str]
    """
    model = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(model, COPASI.CDataModel))
    result = []
    for task in model.getTaskList():
        if task.isScheduled():
            result.append(task.getObjectName())
    return result


def set_scheduled_tasks(task_name, **kwargs):
    """Sets the scheduled tasks

    Only the tasks with the listed names will be set to be scheduled.

    :param task_name: name or list of names of tasks set to be scheduled
    :type task_name: str or [str]
    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return: None
    """
    model = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(model, COPASI.CDataModel))

    if isinstance(task_name, str):
        task_name = [task_name]

    for c_task in model.getTaskList():
        assert (isinstance(c_task, COPASI.CCopasiTask))
        c_task.setScheduled(c_task.getObjectName() in task_name)


def _invert_dict(d):
    """Utility function that inverts / reverses the given dictionary

    :param d: the dictionary to invert / revert
    :type d: dict

    :return: a dictionary that has the values as keys, and keys as values of d
    :rtype: dict
    """
    result = {}

    for k, v in d.items():
        result[v] = k

    return result


def _get_name_for_object(obj, model):
    """ Tries to get a nice display name for the given object, but falls back to cn, if name cannot be resolved

    :param obj: the copasi object to get the name for
    :type obj: COPASI.CDataObject

    :param model: the data model in which to resolve the object
    :type model: COPASI.CDataModel

    :return: name or cn of the object
    :rtype: str
    """
    name = obj.getObjectDisplayName()
    resolved = model.findObjectByDisplayName(name)

    if resolved is None:
        return obj.getCN().getString()

    if resolved.getCN().getString() != obj.getCN().getString():
        return obj.getCN().getString()

    return name


def as_dict(df, fold_list=True):
    """Convenience function returning the data frame as dictionary

    :param df: the data frame
    :type df: pd.DataFrame
    :param fold_list: optional boolean indicating whether to fold lists into a single dictionary,
                      if there is only one entry (defaults to true)
    :type fold_list: bool

    :return: the contents of the dataframe as [{}] if there are multiple ones,
             otherwise the dictionary if just one, or None
    :rtype: List[Dict] or Dict or None
    """
    if df is None:
        return None

    res = df.reset_index().to_dict(orient='records')
    if not res:
        return None

    if fold_list and len(res) == 1:
        return res[0]

    return res


def get_parameter_sets(name=None, exact=False, values_only=False, **kwargs):
    """
    Returns the list of parameter sets

    :param name: name of the parameter set to return (or a substring of the name)
    :type name: str

    :param exact: boolean indicating whether the name has to be exact or not (default: False)
    :type exact: bool

    :param values_only: boolean indicating whether to return only the values of the entries of the
                parameter set or a dictionary describing it (default: False)
    :type values_only: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    result = []

    sets = model.getModelParameterSets()
    assert (isinstance(sets, COPASI.ModelParameterSetVectorN))

    for i in range(sets.size()):
        pset = sets.get(i)
        set_name = pset.getObjectName()

        if name and name not in set_name:
            continue

        if name and type(name) is str and exact and name != set_name:
            continue

        result.append(_parameterset_to_dict(pset, dm, values_only))

    return result


def remove_parameter_sets(name=None, exact=False, **kwargs):
    """
    remove the named parameter set(s)

    :param name: name of the parameter set to remove (or a substring of the name)
    :type name: str

    :param exact: boolean indicating whether the name has to be exact
    :type exact: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    sets = model.getModelParameterSets()
    assert (isinstance(sets, COPASI.ModelParameterSetVectorN))

    num_sets = sets.size()

    for i in reversed(range(num_sets)):
        pset = sets.get(i)
        set_name = pset.getObjectName()

        if name and name not in set_name:
            continue

        if name and type(name) is str and exact and name != set_name:
            continue

        sets.remove(i)


def add_parameter_set(name, param_set_dict=None, **kwargs):
    """
    Adds a new parameter set to the model with the values from the dictionary

    :param name: name of the parameter set to add
    :type name: str

    :param param_set_dict: dictionary with the parameter set values if empty, the
                           current state of the model will be used
    :type param_set_dict: dict or None

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    sets = model.getModelParameterSets()
    assert (isinstance(sets, COPASI.ModelParameterSetVectorN))

    if sets.getByName(name) is not None:
        raise ValueError('Parameter set with name "{}" already exists'.format(name))

    # if we are here, the parameter set does not exist yet, unfortunately
    # the test added a message, so lets remove it
    if COPASI.CCopasiMessage.peekLastMessage().getNumber() == 5501:
        COPASI.CCopasiMessage.getLastMessage()

    if param_set_dict is None:
        # create parameter set from current state
        new_set = COPASI.CModelParameterSet(name)
        sets.addAndOwn(new_set)
        new_set.createFromModel()
        return

    new_set = COPASI.CModelParameterSet(name)
    sets.addAndOwn(new_set)

    _set_parameter_set(new_set, param_set_dict, dm, False)


def set_parameter_set(name, exact=False, param_set_dict=None, remove_others=False, **kwargs):
    """
    sets the named parameter sets to the given dictionary values

    :param name: name of the parameter set to change (or a substring of the name)
    :type name: str

    :param exact: boolean indicating whether the name has to be exact
    :type exact: bool

    :param param_set_dict: dictionary with the parameter set values
    :type param_set_dict: dict or None

    :param remove_others: boolean indicating whether to remove entries, that are not specified in the dictionary
                          (default: False)
    :type remove_others: bool

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    _run_functions_on_selected_parameter_sets(
        name, exact, False,
        _set_parameter_set,
        param_set_dict, dm, remove_others, True,
        **kwargs
    )


def _set_parameter_set(p_set, param_set_dict, dm, remove_others, rename_set=False):
    """ CHanges the specified parameter set

    :param p_set: the parameter set to change
    :param param_set_dict:  the dictionary with new values
    :param dm: the data model
    :param remove_others: boolean indicating whether to remove entries, that are not specified in the dictionary
    :param rename_set: boolean indicating whether to rename the parameter set
    :return: None
    """
    if not p_set:
        return

    if not param_set_dict:
        return

    for key in param_set_dict:
        if key == 'description':
            p_set.setNotes(param_set_dict['description'])
            continue

        if key == 'name' and rename_set:
            p_set.setObjectName(param_set_dict['name'])
            continue

        param = p_set.getModelParameter(COPASI.CDataString(key).getCN())
        if not param:
            continue

        _update_paramgroup(param, param_set_dict[key], key, dm, remove_others)

    p_set.compile()


def _update_paramgroup(param, group_dict, name, dm, remove_others):
    """ Updates the given model parameter group

    :param param: the parameter group to update
    :type param: COPASI.CModelParameterGroup
    :param group_dict: the dictionary with new values
    :param name: name of the group
    :param dm: the data model
    :param remove_others: boolean indicating whether to remove entries, that are not specified in the dictionary
    :return:
    """
    if remove_others:
        param.clear()
    for key in group_dict:
        current = group_dict[key]
        if isinstance(current, dict):
            value = current['value'] if 'value' in current else None
        else:
            value = float(current)
        p_type = _group_to_ptype_int(name, current)
        s_type = _guess_simulation_type(p_type, current)

        obj = _get_object_by_ptype(p_type, key, dm)
        if not obj:
            logger.warning('Could not find object for "{}", skipping'.format(key))
            continue

        new_param = param.getModelParameter(obj.getCN())
        if not new_param:
            new_param = param.add(p_type)
            new_param.setCN(obj.getCN())
            new_param.setSimulationType(s_type)

        if p_type == COPASI.CModelParameter.Type_Model:
            new_param.setValue(value)
            continue

        if p_type == COPASI.CModelParameter.Type_ModelValue:
            new_param.setValue(value)
            continue

        if p_type == COPASI.CModelParameter.Type_Compartment:
            new_param.setValue(value, COPASI.CCore.Framework_Concentration)
            continue

        if p_type == COPASI.CModelParameter.Type_Species:
            if isinstance(current, dict):
                if 'particle_number' in current:
                    new_param.setValue(current['particle_number'], COPASI.CCore.Framework_ParticleNumbers)
                if 'concentration' in current:
                    new_param.setValue(current['concentration'], COPASI.CCore.Framework_Concentration)
            else:
                new_param.setValue(value, COPASI.CCore.Framework_Concentration)
            continue

        if p_type == COPASI.CModelParameter.Type_Reaction:
            for param_key in current:
                current_p = current[param_key]
                if isinstance(current_p, dict):
                    value = current_p['value'] if 'value' in current_p else None
                    p_type = _parameter_type_to_int(
                        current_p['parameter_type']) if 'parameter_type' in current_p else None
                    s_type = __status_to_int(current_p['simulation_type']) if 'simulation_type' in current_p else None
                else:
                    value = float(current_p)
                    p_type = COPASI.CModelParameter.Type_ReactionParameter
                    s_type = COPASI.CModelEntity.Status_FIXED

                param_name = '({}).{}'.format(key, param_key)
                local_param_obj = dm.findObjectByDisplayName(param_name)

                if not local_param_obj:
                    logger.warning('Could not find local parameter "{}", skipping'.format(param_name))
                    continue

                local_param = new_param.add(p_type)
                local_param.setCN(local_param_obj.getCN())
                local_param.setValue(value)
                local_param.setSimulationType(s_type)


def apply_parameter_set(name, exact=False, **kwargs):
    """ Applies the parameter set with the given name to the model

    :param name: the name of the parameter set or a substring of the name
    :param exact: boolean indicating whether the name has to match exactly

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:
    """
    _run_functions_on_selected_parameter_sets(
        name, exact, False,
        lambda pset: pset.updateModel(),
        **kwargs
    )


def _run_functions_on_selected_parameter_sets(_name, _exact, _reversed, function, *args, **kwargs):
    """ Runs the given function on all parameter sets in the model

    :param dm: the data model
    :param function: the function to run
    :param args: arguments for the function
    :param kwargs: keyword arguments for the function
    :return:
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    assert (isinstance(dm, COPASI.CDataModel))

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    sets = model.getModelParameterSets()
    assert (isinstance(sets, COPASI.ModelParameterSetVectorN))

    num_sets = sets.size()
    this_range = range(num_sets)
    if _reversed:
        this_range = reversed(this_range)
    for i in this_range:
        pset = sets.get(i)
        set_name = pset.getObjectName()

        if _name and _name not in set_name:
            continue

        if _name and type(_name) is str and _exact and _name != set_name:
            continue

        function(pset, *args, **kwargs)


def update_parameter_set(name, exact=False, **kwargs):
    """ Updates the specified parameter set with values from the model

    :param name: the name of the parameter set or a substring of the name
    :param exact: boolean indicating whether the name has to match exactly

    :param kwargs: optional parameters

        - | `model`: to specify the data model to be used (if not specified
          | the one from :func:`.get_current_model` will be taken)

    :return:

    """
    _run_functions_on_selected_parameter_sets(
        name, exact, False,
        lambda pset: pset.refreshFromModel(True), **kwargs)


def _get_object_by_ptype(p_type, name, dm):
    if p_type == COPASI.CModelParameter.Type_Model:
        return dm.getModel()
    if p_type == COPASI.CModelParameter.Type_ModelValue:
        return dm.getModel().getModelValue(name)
    if p_type == COPASI.CModelParameter.Type_Compartment:
        return dm.getModel().getCompartment(name)
    if p_type == COPASI.CModelParameter.Type_Species:
        return dm.getModel().getMetabolite(name)
    if p_type == COPASI.CModelParameter.Type_Reaction:
        return dm.getModel().getReaction(name)
    return None


def _parametergroup_to_dict(pgroup, dm, values_only):
    result = {}
    for i in range(pgroup.size()):
        child = pgroup.getChild(i)
        if isinstance(child, COPASI.CModelParameterGroup):
            result[child.getName()] = _parametergroup_to_dict(child, dm, values_only)
            continue

        if values_only:
            if isinstance(child, COPASI.CModelParameterSpecies):
                result[child.getName()] = child.getValue(COPASI.CCore.Framework_Concentration)
            else:
                result[child.getName()] = child.getValue()
            continue

        new_dict = {}
        if isinstance(child, COPASI.CModelParameterSpecies):
            new_dict['concentration'] = child.getValue(COPASI.CCore.Framework_Concentration)
            new_dict['particle_number'] = child.getValue(COPASI.CCore.Framework_ParticleNumbers)
        else:
            new_dict['value'] = child.getValue()

        new_dict['parameter_type'] = _parameter_type_to_string(child.getType())
        new_dict['simulation_type'] = __status_to_string(child.getSimulationType())

        result[child.getName()] = new_dict

    return result


def _group_to_ptype_int(group_name, value_or_dict):
    if isinstance(value_or_dict, dict):
        p_type = _parameter_type_to_int(value_or_dict['parameter_type']) if 'parameter_type' in value_or_dict else None
        if p_type:
            return p_type

    values = {
        'Initial Time': COPASI.CModelParameter.Type_Model,
        "Initial Compartment Sizes": COPASI.CModelParameter.Type_Compartment,
        "Initial Species Values": COPASI.CModelParameter.Type_Species,
        "Initial Global Quantities": COPASI.CModelParameter.Type_ModelValue,
        "Kinetic Parameters": COPASI.CModelParameter.Type_Reaction,
    }

    return values.get(group_name, COPASI.CModelParameter.Type_Reaction)


def _guess_simulation_type(p_type, value_or_dict):
    if isinstance(value_or_dict, dict):
        s_type = __status_to_int(value_or_dict['simulation_type']) if 'simulation_type' in value_or_dict else None
        if s_type:
            return s_type

    if p_type == COPASI.CModelParameter.Type_Model:
        s_type = COPASI.CModelEntity.Status_TIME
    elif p_type == COPASI.CModelParameter.Type_Species:
        s_type = COPASI.CModelEntity.Status_REACTIONS
    else:
        s_type = COPASI.CModelEntity.Status_FIXED

    return s_type


def _parameter_type_to_string(p_type):
    # type: (int)->str
    strings = {
        COPASI.CModelParameter.Type_Model: "model",
        COPASI.CModelParameter.Type_ReactionParameter: "reaction_parameter",
        COPASI.CModelParameter.Type_Reaction: "reaction",
        COPASI.CModelParameter.Type_Set: "set",
        COPASI.CModelParameter.Type_Group: "group",
        COPASI.CModelParameter.Type_Compartment: "compartment",
        COPASI.CModelParameter.Type_ModelValue: "parameter",
        COPASI.CModelParameter.Type_Species: "species",
        COPASI.CModelParameter.Type_unknown: "unknown",
    }
    return strings.get(p_type, 'unknown')


def _parameter_type_to_int(p_type):
    # type: (str)->int
    values = {
        "model": COPASI.CModelParameter.Type_Model,
        "reaction_parameter": COPASI.CModelParameter.Type_ReactionParameter,
        "reaction": COPASI.CModelParameter.Type_Reaction,
        "set": COPASI.CModelParameter.Type_Set,
        "group": COPASI.CModelParameter.Type_Group,
        "compartment": COPASI.CModelParameter.Type_Compartment,
        "parameter": COPASI.CModelParameter.Type_ModelValue,
        "species": COPASI.CModelParameter.Type_Species,
        "unknown": COPASI.CModelParameter.Type_unknown,
    }
    return values.get(p_type, COPASI.CModelParameter.Type_unknown)


def _parameterset_to_dict(pset, dm, values_only):
    """
    Converts the given parameter set to a dictionary

    :param pset: the parameter set
    :type pset: COPASI.CModelParameterSet
    :param dm: the datamodel
    :return: dictionary of the parameter set
    """

    result = {'name': pset.getObjectName(),
              'description': pset.getNotes()}

    for entry in [
        'Initial Time',
        "Initial Compartment Sizes",
        "Initial Species Values",
        "Initial Global Quantities",
        "Kinetic Parameters"
    ]:
        result[entry] = _parametergroup_to_dict(
            pset.getModelParameter(COPASI.CDataString(entry).getCN()), dm, values_only)

    return result


def _simplify_name(name, drop=None):
    """
    Simplifies the given name by removing all special characters and replacing them with underscores
    additionally a list of substrings can be given that will be removed from the name

    :param name: the name to simplify
    :type name: str
    :param drop: a list of substrings to drop
    :type drop: list[str] or None
    :return: the simplified name
    """
    if drop:
        for substr in drop:
            name = name.replace(substr, '')

    # replace all non-alphanumeric characters with underscores
    name = re.sub(r'[^a-zA-Z0-9_]', '_', name)

    # remove double underscores
    name = name.replace('__', '_')

    # remove trailing underscores
    while name.endswith('_'):
        name = name[:-1]

    if drop:
        for substr in drop:
            name = name.replace(substr, '')

    return name


def simplify_names(**kwargs):
    """
    Simplifies the names of the model elements by removing all special characters and replacing them with underscores

    :param kwargs:

    - model: the model to simplify
    - drop: a list of substrings to drop from the names

    :return: None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)
    drop = kwargs.get('drop', None)

    model = dm.getModel()
    assert (isinstance(model, COPASI.CModel))

    for i in range(model.getNumReactions()):
        reaction = model.getReaction(i)
        reaction.setObjectName(_simplify_name(reaction.getObjectName(), drop))

    for i in range(model.getNumMetabs()):
        metab = model.getMetabolite(i)
        metab.setObjectName(_simplify_name(metab.getObjectName(), drop))

    for i in range(model.getNumCompartments()):
        comp = model.getCompartment(i)
        comp.setObjectName(_simplify_name(comp.getObjectName(), drop))

    for i in range(model.getNumModelValues()):
        gq = model.getModelValue(i)
        gq.setObjectName(_simplify_name(gq.getObjectName(), drop))


def run_scheduled_tasks(include_plots=True, include_general_plots=False, plots=None, reports=None, **kwargs):
    """
    Runs all scheduled tasks, optionally producing plots and reports

    :param include_plots: boolean indicating whether to produce the plots associated with the task
    :param include_general_plots: boolean indicating whether to produce the general plots
    :param plots: optional list of plot dataframes computed by the task
    :param reports: optional list of report dataframes computed by the task
    :return: Figure produced if `include_plots` is true, otherwise None
    """
    for name in get_scheduled_tasks():
        run_task(name, include_plots, include_general_plots, plots, reports, **kwargs)


def _create_plot(plot_spec, data):
    """
    Creates a plot from the given plot specification and data

    :param plot_spec: the plot specification as dictionary produced from get_plot_specification
    :param data: pandas dataframe with the data for the plot
    :return: figure and axes element as tuple
    """
    cn_to_index = {}
    count = 0
    fig = plt.figure()
    ax = plt.subplot(111)
    for curve in plot_spec['curves']:
        for channel in curve['channels']:
            cn_to_index[channel] = count
            count += 1

        ax.plot(data[cn_to_index[curve['channels'][0]]].values, data[cn_to_index[curve['channels'][1]]].values,
                label=curve['name'])
    if plot_spec['log_x']:
        ax.set_xscale('log')
    if plot_spec['log_y']:
        ax.set_yscale('log')

    fig.suptitle(plot_spec['name'])

    # Shrink current axis's height by 10% on the bottom
    box = ax.get_position()
    ax.set_position([box.x0, box.y0 + box.height * 0.1,
                     box.width, box.height * 0.9])
    # add legend below
    ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.05),
              fancybox=True, shadow=True, ncol=5)
    return fig, ax


def run_task(task_name, include_plots=True, include_general_plots=False, plots=None, reports=None, **kwargs):
    """
    Utility function that runs the named task and returns the result

    :param task_name: the name of the task e.g. 'Time-Course' to run the timecourse task. See the
        basico.T for all the task names.
    :type task_name: str
    :param include_plots: boolean indicating whether to produce the plots associated with the task
    :type include_plots: bool
    :param include_general_plots: boolean indicating whether to produce the general plots (those not specified to
         a particular task)
    :type include_general_plots: bool
    :param plots: optional list of plot dataframes computed by the task
    :type plots: list[pandas.DataFrame] or None
    :param reports: optional list of report dataframes computed by the task
    :type reports: list[pandas.DataFrame] or None
    :return: Figures produced if `include_plots` is true, otherwise None
    """
    dm = model_io.get_model_from_dict_or_default(kwargs)

    report_defs = []
    plot_defs = []

    # find reports and plots
    for i in range(dm.getNumReportDefinitions()):
        spec = get_report_dict(i)
        if spec is None:
            continue
        if spec['task'] != task_name:
            continue
        report_defs.append(spec)

    for i in range(dm.getNumPlotSpecifications()):
        spec = get_plot_dict(i)
        if spec is None:
            continue
        if not spec['tasks']:
            if not include_general_plots:
                continue
        elif task_name not in spec['tasks']:
            continue

        plot_defs.append(spec)

    # create data handlers for reports and plots
    report_handlers = []
    if reports is not None and report_defs:
        for spec in report_defs:
            if spec['is_table']:
                # just go through the table entries and add them to handler
                pass
            else:
                # add header, body and footer separately
                pass

    plot_handlers = []
    for spec in plot_defs:
        dh = COPASI.CDataHandler()
        for curve in spec['curves']:
            if curve['activity'] != 'during':
                continue
            for channel in curve['channels']:
                dh.addDuringName(COPASI.CRegisteredCommonName(channel))
            pass
        plot_handlers.append({
            'handler': dh,
            'spec': spec
        })

    # bail if no outputs
    if not report_handlers and not plot_handlers:
        logger.error('No reports or plots for the task {}'.format(task_name))
        return

    for handler in report_handlers + plot_handlers:
        dm.addInterface(handler['handler'])

    # run task
    task = dm.getTask(task_name)
    task.initializeRawWithOutputHandler(COPASI.CCopasiTask.OUTPUT_UI, dm)
    task.processRaw(True)
    task.restore()

    for handler in report_handlers + plot_handlers:
        dm.removeInterface(handler['handler'])

    # produce dataframes for reports and plots
    for handler in report_handlers:
        pass
    for handler in plot_handlers:
        data = []
        for i in range(handler['handler'].getNumRowsDuring()):
            data.append(handler['handler'].getNthRow(i))
        handler['df'] = pd.DataFrame(data)
        if plots is not None:
            plots.append(handler['df'])
    # produce plots
    if not include_plots:
        return

    figures = []
    for handler in plot_handlers:
        fig, ax = _create_plot(handler['spec'], handler['df'])
        figures.append((fig, ax))

    return figures


def get_copasi_messages(num_messages_before, filters=None):
    """ Returns error messages that occurred while initializing or running the simulation

    :param num_messages_before: number of messages before calling initialization or process
    :param filters: optional list of filter expressions of what messages to ignore
    :type filters: list of str or str or None

    :return: error messages in form of a string

    """
    messages = []
    if filters is None:
        filters = []
    if type(filters) == str:
        filters = [filters]
    while COPASI.CCopasiMessage.size() > num_messages_before:
        message = COPASI.CCopasiMessage.getLastMessage()
        skip = False
        for filter_text in filters:
            if filter_text in message.getText():
                skip = True
                break
        if not skip:
            messages.insert(0, message.getText())
    return "".join(messages)
