"""
GSLCodeGenerators for code that uses the ODE solver provided by the GNU Scientific Library (GSL)
"""

import os
import re

import numpy as np

from brian2.codegen.generators import c_data_type
from brian2.codegen.permutation_analysis import (
    OrderDependenceError,
    check_for_order_independence,
)
from brian2.codegen.translation import make_statements
from brian2.core.functions import Function
from brian2.core.preferences import BrianPreference, PreferenceError, prefs
from brian2.core.variables import ArrayVariable, AuxiliaryVariable, Constant
from brian2.parsing.statements import parse_statement
from brian2.units.fundamentalunits import fail_for_dimension_mismatch
from brian2.utils.logger import get_logger
from brian2.utils.stringtools import get_identifiers, word_substitute

__all__ = ["GSLCodeGenerator", "GSLCPPCodeGenerator", "GSLCythonCodeGenerator"]


logger = get_logger(__name__)


def valid_gsl_dir(val):
    """
    Validate given string to be path containing required GSL files.
    """
    if val is None:
        return True
    if not isinstance(val, str):
        raise PreferenceError(
            f"Illegal value for GSL directory: {str(val)}, has to be str"
        )
    if not os.path.isdir(val):
        raise PreferenceError(
            f"Illegal value for GSL directory: {val}, has to be existing directory"
        )
    if any(
        not os.path.isfile(os.path.join(val, "gsl", filename))
        for filename in ["gsl_odeiv2.h", "gsl_errno.h", "gsl_matrix.h"]
    ):
        raise PreferenceError(
            f"Illegal value for GSL directory: '{val}', "
            "has to contain gsl_odeiv2.h, gsl_errno.h "
            "and gsl_matrix.h"
        )
    return True


prefs.register_preferences(
    "GSL",
    "Directory containing GSL code",
    directory=BrianPreference(
        validator=valid_gsl_dir,
        docs=(
            "Set path to directory containing GSL header files (gsl_odeiv2.h etc.)"
            "\nIf this directory is already in Python's include (e.g. because of "
            "conda installation), this path can be set to None."
        ),
        default=None,
    ),
)


class GSLCodeGenerator:
    """
    GSL code generator.

    Notes
    -----
    Approach is to first let the already existing code generator for a target
    language do the bulk of the translating from abstract_code to actual code.
    This generated code is slightly adapted to render it GSL compatible.
    The most critical part here is that the vector_code that is normally
    contained in a loop in the ```main()``` is moved to the function ```_GSL_func```
    that is sent to the GSL integrator. The variables used in the vector_code are
    added to a struct named ```dataholder``` and their values are set from the
    Brian namespace just before the scalar code block.
    """

    def __init__(
        self,
        variables,
        variable_indices,
        owner,
        iterate_all,
        codeobj_class,
        name,
        template_name,
        override_conditional_write=None,
        allows_scalar_write=False,
    ):
        self.generator = codeobj_class.original_generator_class(
            variables,
            variable_indices,
            owner,
            iterate_all,
            codeobj_class,
            name,
            template_name,
            override_conditional_write,
            allows_scalar_write,
        )
        self.method_options = dict(owner.state_updater.method_options)
        self.integrator = owner.state_updater.integrator
        # default timestep to start with is the timestep of the NeuronGroup itself
        self.method_options["dt_start"] = owner.dt.variable.get_value()[0]
        self.variable_flags = owner.state_updater._gsl_variable_flags

    def __getattr__(self, item):
        return getattr(self.generator, item)

    # A series of functions that should be overridden by child class:
    def c_data_type(self, dtype):
        """
        Get string version of object dtype that is attached to Brian variables. c
        pp_generator already has this function, but the Cython generator does not,
        but we need it for GSL code generation.
        """
        return NotImplementedError

    def initialize_array(self, varname, values):
        """
        Initialize a static array with given floating point values. E.g. in C++,
        when called with arguments ``array`` and ``[1.0, 3.0, 2.0]``, this
        method should return ``double array[] = {1.0, 3.0, 2.0}``.

        Parameters
        ----------
        varname : str
            The name of the array variable that should be initialized
        values : list of float
            The values that should be assigned to the array

        Returns
        -------
        code : str
            One or more lines of array initialization code.
        """
        raise NotImplementedError

    def var_init_lhs(self, var, type):
        """
        Get string version of the left hand side of an initializing expression

        Parameters
        ----------
        var : str
        type : str

        Returns
        -------
        code : str
            For cpp returns type + var, while for cython just var
        """
        raise NotImplementedError

    def unpack_namespace_single(self, var_obj, in_vector, in_scalar):
        """
        Writes the code necessary to pull single variable out of the Brian
        namespace into the generated code.

        The code created is significantly different between cpp and cython,
        so I decided to not make this function general
        over all target languages (i.e. in contrast to most other functions
        that only have syntactical differences)
        """
        raise NotImplementedError

    # GSL functions that are the same for all target languages:
    def find_function_names(self):
        """
        Return a list of used function names in the self.variables dictionary

        Functions need to be ignored in the GSL translation process, because the
        brian generator already sufficiently
        dealt with them. However, the brian generator also removes them from the
        variables dict, so there is no
        way to check whether an identifier is a function after the brian
        translation process. This function is called
        before this translation process and the list of function names is stored
        to be used in the GSL translation.

        Returns
        -------
        function_names : list
            list of strings that are function names used in the code
        """
        variables = self.variables
        return [
            var for var, var_obj in variables.items() if isinstance(var_obj, Function)
        ]

    def is_cpp_standalone(self):
        """
        Check whether we're running with cpp_standalone.

        Test if `get_device()` is instance `CPPStandaloneDevice`.

        Returns
        -------
        is_cpp_standalone : bool
            whether currently using cpp_standalone device

        See Also
        --------
        is_constant_and_cpp_standalone : uses the returned value
        """
        # imports here to avoid circular imports
        from brian2.devices.cpp_standalone.device import CPPStandaloneDevice
        from brian2.devices.device import get_device

        device = get_device()
        return isinstance(device, CPPStandaloneDevice)

    def is_constant_and_cpp_standalone(self, var_obj):
        """Check whether self.cpp_standalone and variable is Constant.

        This check is needed because in the case of using the cpp_standalone device we do not
        want to apply our GSL variable conversion (var --> _GSL_dataholder.var), because the cpp_standalone
        code generation process involves replacing constants with their actual value ('freezing').
        This results in code that looks like (if for example var = 1.2): _GSL_dataholder.1.2 = 1.2 and _GSL_dataholder->1.2.
        To prevent repetitive calls to get_device() etc. the outcome of is_cpp_standalone is saved.

        Parameters
        ----------
        var_obj : `Variable`
            instance of brian Variable class describing the variable

        Returns
        -------
        is_cpp_standalone : bool
            whether the used device is cpp_standalone and the given variable is an instance of Constant
        """
        if not hasattr(self, "cpp_standalone"):
            self.cpp_standalone = self.is_cpp_standalone()
        return isinstance(var_obj, Constant) and self.cpp_standalone

    def find_differential_variables(self, code):
        """
        Find the variables that were tagged _gsl_{var}_f{ind} and return var, ind pairs.

        `GSLStateUpdater` tagged differential variables and here we extract the information given in these tags.

        Parameters
        ----------
        code : list of strings
            A list of strings containing gsl tagged variables

        Returns
        -------
        diff_vars : dict
            A dictionary with variable names as keys and differential equation index as value
        """
        diff_vars = {}
        for expr_set in code:
            for expr in expr_set.split("\n"):
                expr = expr.strip(" ")
                try:
                    lhs, op, rhs, comment = parse_statement(expr)
                except ValueError:
                    pass
                m = re.search("_gsl_(.+?)_f([0-9]*)$", lhs)
                if m:
                    diff_vars[m.group(1)] = m.group(2)
        return diff_vars

    def diff_var_to_replace(self, diff_vars):
        """
        Add differential variable-related strings that need to be replaced to go
        from normal brian to GSL code

        From the code generated by Brian's 'normal' generators (cpp_generator or
        cython_generator a few bits of text need to be replaced to get GSL
        compatible code. The bits of text related to differential equation
        variables are put in the replacer dictionary in this function.

        Parameters
        ----------
        diff_vars : dict
            dictionary with variables as keys and differential equation index as value

        Returns
        -------
        to_replace : dict
            dictionary with strings that need to be replaced as keys and the
            strings that will replace them as values
        """
        variables = self.variables
        to_replace = {}
        for var, diff_num in list(diff_vars.items()):
            to_replace.update(self.var_replace_diff_var_lhs(var, diff_num))
            var_obj = variables[var]
            array_name = self.generator.get_array_name(var_obj, access_data=True)
            idx_name = "_idx"  # TODO: could be dynamic?
            replace_what = f"{var} = {array_name}[{idx_name}]"
            replace_with = f"{var} = _GSL_y[{diff_num}]"
            to_replace[replace_what] = replace_with
        return to_replace

    def get_dimension_code(self, diff_num):
        """
        Generate code for function that sets the dimension of the ODE system.

        GSL needs to know how many differential variables there are in the
        ODE system. Since the current approach is to have the code in the vector
        loop the same for all simulations, this dimension is set by an external
        function. The code for this set_dimension functon is written here.
        It is assumed the code will be the same for each target language with the
        exception of some syntactical differences

        Parameters
        ----------
        diff_num : int
            Number of differential variables that describe the ODE system

        Returns
        -------
        set_dimension_code : str
            The code describing the target language function in a single string
        """
        code = ["\n{start_declare}int set_dimension(size_t * dimension){open_function}"]
        code += ["\tdimension[0] = %d{end_statement}" % diff_num]
        code += ["\treturn GSL_SUCCESS{end_statement}{end_function}"]
        return ("\n").join(code).format(**self.syntax)

    def yvector_code(self, diff_vars):
        """
        Generate code for function dealing with GSLs y vector.

        The values of differential variables have to be transferred from
        Brian's namespace to a vector that is given to GSL. The transferring
        from Brian --> y and back from y --> Brian after integration happens in
        separate functions. The code for these is written here.

        Parameters
        ----------
        diff_vars : dictionary
            Dictionary containing variable names as keys (str) and differential
            variable index as value

        Returns
        -------
        yvector_code : str
            The code for the two functions (``_fill_y_vector`` and
            ``_empty_y_vector``) as single string.
        """
        fill_y = [
            "\n{start_declare}int _fill_y_vector(_dataholder *"
            "_GSL_dataholder, double * _GSL_y, int _idx){open_function}"
        ]
        empty_y = [
            "\n{start_declare}int _empty_y_vector(_dataholder * "
            "_GSL_dataholder, double * _GSL_y, int _idx){"
            "open_function}"
        ]
        for var, diff_num in list(diff_vars.items()):
            diff_num = int(diff_num)
            array_name = self.generator.get_array_name(
                self.variables[var], access_data=True
            )
            fill_y += [
                "\t_GSL_y[%d] = _GSL_dataholder{access_pointer}%s[_idx]{end_statement}"
                % (diff_num, array_name)
            ]
            empty_y += [
                "\t_GSL_dataholder{access_pointer}%s[_idx] = _GSL_y[%d]{end_statement}"
                % (array_name, diff_num)
            ]
        fill_y += ["\treturn GSL_SUCCESS{end_statement}{end_function}"]
        empty_y += ["\treturn GSL_SUCCESS{end_statement}{end_function}"]
        return ("\n").join(fill_y + empty_y).format(**self.syntax)

    def make_function_code(self, lines):
        """
        Add lines of GSL translated vector code to 'non-changing' _GSL_func code.

        Adds nonchanging aspects of GSL _GSL_func code to lines of code
        written somewhere else (`translate_vector_code`). Here these lines
        are put between the non-changing parts of the code and the
        target-language specific syntax is added.

        Parameters
        ----------
        lines : str
            Code containing GSL version of equations

        Returns
        -------
        function_code : str
            code describing ``_GSL_func`` that is sent to GSL integrator.
        """
        code = [
            "\n{start_declare}int _GSL_func(double t, const double "
            "_GSL_y[], double f[], void * params){open_function}"
            "\n\t{start_declare}_dataholder * _GSL_dataholder = {open_cast}"
            "_dataholder *{close_cast} params{end_statement}"
            "\n\t{start_declare}int _idx = _GSL_dataholder{access_pointer}_idx"
            "{end_statement}"
        ]
        code += [lines]
        code += ["\treturn GSL_SUCCESS{end_statement}{end_function}"]
        return ("\n").join(code).format(**self.syntax)

    def write_dataholder_single(self, var_obj):
        """
        Return string declaring a single variable in the ``_dataholder`` struct.

        Parameters
        ----------
        var_obj : `Variable`

        Returns
        -------
        code : str
            string describing this variable object as required for the ``_dataholder`` struct
            (e.g. ``double* _array_neurongroup_v``)
        """
        dtype = self.c_data_type(var_obj.dtype)
        if isinstance(var_obj, ArrayVariable):
            pointer_name = self.get_array_name(var_obj, access_data=True)
            try:
                restrict = self.generator.restrict
            except AttributeError:
                restrict = ""
            if var_obj.scalar or var_obj.size == 1:
                restrict = ""
            return f"{dtype}* {restrict} {pointer_name}{{end_statement}}"
        else:
            return f"{dtype} {var_obj.name}{{end_statement}}"

    def write_dataholder(self, variables_in_vector):
        """
        Return string with full code for _dataholder struct.

        Parameters
        ----------
        variables_in_vector : dict
            dictionary containing variable name as key and `Variable` as value

        Returns
        -------
        code : str
            code for _dataholder struct
        """
        code = ["\n{start_declare}struct _dataholder{open_struct}"]
        code += ["\tint _idx{end_statement}"]
        for var, var_obj in list(variables_in_vector.items()):
            if (
                var == "t"
                or "_gsl" in var
                or self.is_constant_and_cpp_standalone(var_obj)
            ):
                continue
            code += [f"	{self.write_dataholder_single(var_obj)}"]
        code += ["{end_struct}"]
        return ("\n").join(code).format(**self.syntax)

    def scale_array_code(self, diff_vars, method_options):
        """
        Return code for definition of ``_GSL_scale_array`` in generated code.

        Parameters
        ----------
        diff_vars : dict
            dictionary with variable name (str) as key and differential variable
            index (int) as value
        method_options : dict
            dictionary containing integrator settings

        Returns
        -------
        code : str
            full code describing a function returning a array containing doubles
            with the absolute errors for each differential variable (according
            to their assigned index in the GSL StateUpdater)
        """
        # get scale values per variable from method_options
        abs_per_var = method_options["absolute_error_per_variable"]
        abs_default = method_options["absolute_error"]

        if not isinstance(abs_default, float):
            raise TypeError(
                "The absolute_error key in method_options should be "
                f"a float. Was type {type(abs_default)}"
            )

        if abs_per_var is None:
            diff_scale = {var: float(abs_default) for var in list(diff_vars.keys())}
        elif isinstance(abs_per_var, dict):
            diff_scale = {}
            for var, error in list(abs_per_var.items()):
                # first do some checks on input
                if var not in diff_vars:
                    if var not in self.variables:
                        raise KeyError(
                            "absolute_error specified for variable that "
                            f"does not exist: {var}"
                        )
                    else:
                        raise KeyError(
                            "absolute_error specified for variable that is "
                            f"not being integrated: {var}"
                        )
                fail_for_dimension_mismatch(
                    error,
                    self.variables[var],
                    "Unit of absolute_error_per_variable "
                    f"for variable {var} does not match "
                    "unit of variable itself",
                )
                # if all these are passed we can add the value for error in base units
                diff_scale[var] = float(error)
            # set the variables that are not mentioned to default value
            for var in list(diff_vars.keys()):
                if var not in abs_per_var:
                    diff_scale[var] = float(abs_default)
        else:
            raise TypeError(
                "The absolute_error_per_variable key in method_options "
                "should either be None or a dictionary "
                "containing the error for each individual state variable. "
                f"Was type {type(abs_per_var)}"
            )
        # write code
        return self.initialize_array(
            "_GSL_scale_array", [diff_scale[var] for var in sorted(diff_vars)]
        )

    def find_undefined_variables(self, statements):
        r"""
        Find identifiers that are not in ``self.variables`` dictionary.

        Brian does not save the ``_lio_`` variables it uses anywhere. This is
        problematic for our GSL implementation because we save the lio variables
        in the ``_dataholder`` struct (for which we need the datatype of the
        variables). This function adds the left hand side variables that are
        used in the vector code to the variable dictionary as
        `AuxiliaryVariable`\ s (all we need later is the datatype).

        Parameters
        ----------
        statements : list
            list of statement objects (need to have the dtype attribute)

        Notes
        -----
        I keep ``self.variables`` and ``other_variables`` separate so I can
        distinguish what variables are in the Brian namespace and which ones are
        defined in the code itself.
        """
        variables = self.variables
        other_variables = {}
        for statement in statements:
            var = statement.var
            if var not in variables:
                other_variables[var] = AuxiliaryVariable(var, dtype=statement.dtype)
        return other_variables

    def find_used_variables(self, statements, other_variables):
        """
        Find all the variables used on the right hand side of the given
        expressions.

        Parameters
        ----------
        statements : list
            list of statement objects

        Returns
        -------
        used_variables : dict
            dictionary of variables that are used as variable name (str),
            `Variable` pairs.
        """
        variables = self.variables
        used_variables = {}
        for statement in statements:
            rhs = statement.expr
            for var in get_identifiers(rhs):
                if var in self.function_names:
                    continue
                try:
                    var_obj = variables[var]
                except KeyError:
                    var_obj = other_variables[var]
                used_variables[var] = var_obj  # save as object because this has
                # all needed info (dtype, name, isarray)

        # I don't know a nicer way to do this, the above way misses write
        # variables (e.g. not_refractory)..
        read, write, _ = self.array_read_write(statements)
        for var in read | write:
            if var not in used_variables:
                used_variables[var] = variables[var]  # will always be array and
                # thus exist in variables

        return used_variables

    def to_replace_vector_vars(self, variables_in_vector, ignore=frozenset()):
        """
        Create dictionary containing key, value pairs with to be replaced text
        to translate from conventional Brian to GSL.

        Parameters
        ----------
        variables_in_vector : dict
            dictionary with variable name (str), `Variable` pairs of variables
            occurring in vector code
        ignore : set, optional
            set of strings with variable names that should be ignored

        Returns
        -------
        to_replace : dict
            dictionary with strings that need to be replaced i.e. _lio_1 will be
            _GSL_dataholder._lio_1 (in cython) or _GSL_dataholder->_lio_1 (cpp)

        Notes
        -----
        t will always be added because GSL defines its own t.
        i.e. for cpp: {'const t = _ptr_array_defaultclock_t[0];' : ''}
        """
        access_pointer = self.syntax["access_pointer"]
        to_replace = {}
        t_in_code = None
        for var, var_obj in list(variables_in_vector.items()):
            if var_obj.name == "t":
                t_in_code = var_obj
                continue
            if "_gsl" in var or var in ignore:
                continue
            if self.is_constant_and_cpp_standalone(var_obj):
                # does not have to be processed by GSL generator
                self.variables_to_be_processed.remove(var_obj.name)
                continue
            if isinstance(var_obj, ArrayVariable):
                pointer_name = self.get_array_name(var_obj, access_data=True)
                to_replace[pointer_name] = (
                    f"_GSL_dataholder{access_pointer}{pointer_name}"
                )
            else:
                to_replace[var] = f"_GSL_dataholder{access_pointer}{var}"

        # also make sure t declaration is replaced if in code
        if t_in_code is not None:
            t_declare = self.var_init_lhs("t", "const double ")
            array_name = self.get_array_name(t_in_code, access_data=True)
            end_statement = self.syntax["end_statement"]
            replace_what = f"{t_declare} = {array_name}[0]{end_statement}"
            to_replace[replace_what] = ""
            self.variables_to_be_processed.remove("t")

        return to_replace

    def unpack_namespace(
        self, variables_in_vector, variables_in_scalar, ignore=frozenset()
    ):
        """
        Write code that unpacks Brian namespace to cython/cpp namespace.

        For vector code this means putting variables in _dataholder (i.e.
        _GSL_dataholder->var or _GSL_dataholder.var = ...)
        Note that code is written so a variable could occur both in scalar and
        vector code

        Parameters
        ----------
        variables_in_vector : dict
            dictionary with variable name (str), `Variable` pairs of variables
            occurring in vector code
        variables_in_scalar : dict
            dictionary with variable name (str), `Variable` pairs of variables
             occurring in scalar code
        ignore : set, optional
            set of string names of variables that should be ignored

        Returns
        -------
        unpack_namespace_code : str
            code fragment unpacking the Brian namespace (setting variables in
            the _dataholder struct in case of vector)
        """
        code = []
        for var, var_obj in list(self.variables.items()):
            if var in ignore:
                continue
            if self.is_constant_and_cpp_standalone(var_obj):
                continue
            in_vector = var in variables_in_vector
            in_scalar = var in variables_in_scalar
            if in_vector:
                self.variables_to_be_processed.remove(var)
            code += [self.unpack_namespace_single(var_obj, in_vector, in_scalar)]
        return ("\n").join(code)

    def translate_vector_code(self, code_lines, to_replace):
        """
        Translate vector code to GSL compatible code by substituting fragments
        of code.

        Parameters
        ----------
        code_lines : list
            list of strings describing the vector_code
        to_replace: dict
            dictionary with to be replaced strings (see to_replace_vector_vars
            and to_replace_diff_vars)

        Returns
        -------
        vector_code : str
            New code that is now to be added to the function that is sent to the
            GSL integrator
        """
        code = []
        for expr_set in code_lines:
            for line in expr_set.split(
                "\n"
            ):  # every line seperate to make tabbing correct
                code += [f"	{line}"]
        code = ("\n").join(code)
        code = word_substitute(code, to_replace)

        # special substitute because of limitations of regex word boundaries with
        # variable[_idx]
        for from_sub, to_sub in list(to_replace.items()):
            m = re.search(r"\[(\w+)\];?$", from_sub)
            if m:
                code = re.sub(re.sub(r"\[", r"\[", from_sub), to_sub, code)

        if "_gsl" in code:
            raise AssertionError(
                "Translation failed, _gsl still in code (should only "
                "be tag, and should be replaced).\n"
                f"Code:\n{code}"
            )

        return code

    def translate_scalar_code(
        self, code_lines, variables_in_scalar, variables_in_vector
    ):
        """
        Translate scalar code: if calculated variables are used in the vector_code
        their value is added to the variable in the _dataholder.

        Parameters
        ----------
        code_lines : list
            list of strings containing scalar code
        variables_in_vector : dict
            dictionary with variable name (str), `Variable` pairs of variables
            occurring in vector code
        variables_in_scalar : dict
            dictionary with variable name (str), `Variable` pairs of variables
            occurring in scalar code

        Returns
        -------
        scalar_code : str
            code fragment that should be injected in the main before the loop
        """
        code = []
        for line in code_lines:
            m = re.search(r"(\w+ = .*)", line)
            try:
                new_line = m.group(1)
                var, op, expr, comment = parse_statement(new_line)
            except (ValueError, AttributeError):
                code += [line]
                continue
            if var in list(variables_in_scalar.keys()):
                code += [line]
            elif var in list(variables_in_vector.keys()):
                if var == "t":
                    continue
                try:
                    self.variables_to_be_processed.remove(var)
                except KeyError:
                    raise AssertionError(
                        "Trying to process variable named %s by "
                        "putting its value in the _GSL_dataholder "
                        "based on scalar code, but the variable "
                        "has been processed already." % var
                    ) from None
                code += [f"_GSL_dataholder.{var} {op} {expr} {comment}"]
        return "\n".join(code)

    def add_gsl_variables_as_non_scalar(self, diff_vars):
        """
        Add _gsl variables as non-scalar.

        In `GSLStateUpdater` the differential equation variables are substituted
        with GSL tags that describe the information needed to translate the
        conventional Brian code to GSL compatible code. This function tells
        Brian that the variables that contain these tags should always be vector
        variables. If we don't do this, Brian renders the tag-variables as
        scalar if no vector variables are used in the right hand side of the
        expression.

        Parameters
        ----------
        diff_vars : dict
            dictionary with variables as keys and differential equation index as
            value
        """
        for var, ind in list(diff_vars.items()):
            name = f"_gsl_{var}_f{ind}"
            self.variables[name] = AuxiliaryVariable(var, scalar=False)

    def add_meta_variables(self, options):
        if options["use_last_timestep"]:
            try:
                N = self.variables["N"].item()
                self.owner.variables.add_array(
                    "_last_timestep",
                    size=N,
                    values=np.ones(N) * options["dt_start"],
                    dtype=np.float64,
                )
            except KeyError:
                # has already been run
                pass
            self.variables["_last_timestep"] = self.owner.variables.get(
                "_last_timestep"
            )
            pointer_last_timestep = (
                f"{self.get_array_name(self.variables['_last_timestep'])}[_idx]"
            )
        else:
            pointer_last_timestep = None

        if options["save_failed_steps"]:
            N = self.variables["N"].item()
            try:
                self.owner.variables.add_array("_failed_steps", size=N, dtype=np.int32)
            except KeyError:
                # has already been run
                pass
            self.variables["_failed_steps"] = self.owner.variables.get("_failed_steps")
            pointer_failed_steps = (
                f"{self.get_array_name(self.variables['_failed_steps'])}[_idx]"
            )
        else:
            pointer_failed_steps = None

        if options["save_step_count"]:
            N = int(self.variables["N"].get_value())
            try:
                self.owner.variables.add_array("_step_count", size=N, dtype=np.int32)
            except KeyError:
                # has already been run
                pass
            self.variables["_step_count"] = self.owner.variables.get("_step_count")
            pointer_step_count = (
                f"{self.get_array_name(self.variables['_step_count'])}[_idx]"
            )
        else:
            pointer_step_count = None

        return {
            "pointer_last_timestep": pointer_last_timestep,
            "pointer_failed_steps": pointer_failed_steps,
            "pointer_step_count": pointer_step_count,
        }

    def translate(
        self, code, dtype
    ):  # TODO: it's not so nice we have to copy the contents of this function..
        """
        Translates an abstract code block into the target language.
        """
        # first check if user code is not using variables that are also used by GSL
        reserved_variables = [
            "_dataholder",
            "_fill_y_vector",
            "_empty_y_vector",
            "_GSL_dataholder",
            "_GSL_y",
            "_GSL_func",
        ]
        if any([var in self.variables for var in reserved_variables]):
            # import here to avoid circular import
            raise ValueError(
                f"The variables {str(reserved_variables)} are reserved for the GSL"
                " internal code."
            )

        # if the following statements are not added, Brian translates the
        # differential expressions in the abstract code for GSL to scalar statements
        # in the case no non-scalar variables are used in the expression
        diff_vars = self.find_differential_variables(list(code.values()))
        self.add_gsl_variables_as_non_scalar(diff_vars)

        # add arrays we want to use in generated code before self.generator.translate() so
        # brian does namespace unpacking for us
        pointer_names = self.add_meta_variables(self.method_options)

        scalar_statements = {}
        vector_statements = {}
        for ac_name, ac_code in code.items():
            statements = make_statements(
                ac_code, self.variables, dtype, optimise=True, blockname=ac_name
            )
            scalar_statements[ac_name], vector_statements[ac_name] = statements
        for vs in vector_statements.values():
            # Check that the statements are meaningful independent on the order of
            # execution (e.g. for synapses)
            try:
                if self.has_repeated_indices(
                    vs
                ):  # only do order dependence if there are repeated indices
                    check_for_order_independence(
                        vs, self.generator.variables, self.generator.variable_indices
                    )
            except OrderDependenceError:
                # If the abstract code is only one line, display it in full
                if len(vs) <= 1:
                    error_msg = f"Abstract code: '{vs[0]}'\n"
                else:
                    error_msg = (
                        f"{len(vs)} lines of abstract code, first line is: '{vs[0]}'\n"
                    )
                logger.warn(
                    "Came across an abstract code block that may not be "
                    "well-defined: the outcome may depend on the "
                    "order of execution. You can ignore this warning if "
                    "you are sure that the order of operations does not "
                    "matter. " + error_msg
                )

        # save function names because self.generator.translate_statement_sequence
        # deletes these from self.variables but we need to know which identifiers
        # we can safely ignore (i.e. we can ignore the functions because they are
        # handled by the original generator)
        self.function_names = self.find_function_names()

        scalar_code, vector_code, kwds = self.generator.translate_statement_sequence(
            scalar_statements, vector_statements
        )

        ############ translate code for GSL

        # first check if any indexing other than '_idx' is used (currently not supported)
        for code_list in list(scalar_code.values()) + list(vector_code.values()):
            for code in code_list:
                m = re.search(r"\[(\w+)\]", code)
                if m is not None:
                    if m.group(1) != "0" and m.group(1) != "_idx":
                        from brian2.stateupdaters.base import (
                            UnsupportedEquationsException,
                        )

                        raise UnsupportedEquationsException(
                            "Equations result in state "
                            "updater code with indexing "
                            "other than '_idx', which "
                            "is currently not supported "
                            "in combination with the "
                            "GSL stateupdater."
                        )

        # differential variable specific operations
        to_replace = self.diff_var_to_replace(diff_vars)
        GSL_support_code = self.get_dimension_code(len(diff_vars))
        GSL_support_code += self.yvector_code(diff_vars)

        # analyze all needed variables; if not in self.variables: put in separate dic.
        # also keep track of variables needed for scalar statements and vector statements
        other_variables = self.find_undefined_variables(
            scalar_statements[None] + vector_statements[None]
        )
        variables_in_scalar = self.find_used_variables(
            scalar_statements[None], other_variables
        )
        variables_in_vector = self.find_used_variables(
            vector_statements[None], other_variables
        )
        # so that _dataholder holds diff_vars as well, even if they don't occur
        # in the actual statements
        for var in list(diff_vars.keys()):
            if var not in variables_in_vector:
                variables_in_vector[var] = self.variables[var]
        # let's keep track of the variables that eventually need to be added to
        # the _GSL_dataholder somehow
        self.variables_to_be_processed = list(variables_in_vector.keys())

        # add code for _dataholder struct
        GSL_support_code = self.write_dataholder(variables_in_vector) + GSL_support_code
        # add e.g. _lio_1 --> _GSL_dataholder._lio_1 to replacer
        to_replace.update(
            self.to_replace_vector_vars(
                variables_in_vector, ignore=list(diff_vars.keys())
            )
        )
        # write statements that unpack (python) namespace to _dataholder struct
        # or local namespace
        GSL_main_code = self.unpack_namespace(
            variables_in_vector, variables_in_scalar, ["t"]
        )

        # rewrite actual calculations described by vector_code and put them in _GSL_func
        func_code = self.translate_one_statement_sequence(
            vector_statements[None], scalar=False
        )
        GSL_support_code += self.make_function_code(
            self.translate_vector_code(func_code, to_replace)
        )
        scalar_func_code = self.translate_one_statement_sequence(
            scalar_statements[None], scalar=True
        )
        # rewrite scalar code, keep variables that are needed in scalar code normal
        # and add variables to _dataholder for vector_code
        GSL_main_code += f"\n{self.translate_scalar_code(scalar_func_code, variables_in_scalar, variables_in_vector)}"
        if len(self.variables_to_be_processed) > 0:
            raise AssertionError(
                "Not all variables that will be used in the vector "
                "code have been added to the _GSL_dataholder. This "
                "might mean that the _GSL_func is using "
                "uninitialized variables.\n"
                "The unprocessed variables "
                f"are: {self.variables_to_be_processed}"
            )

        scalar_code["GSL"] = GSL_main_code
        kwds["define_GSL_scale_array"] = self.scale_array_code(
            diff_vars, self.method_options
        )
        kwds["n_diff_vars"] = len(diff_vars)
        kwds["GSL_settings"] = dict(self.method_options)
        kwds["GSL_settings"]["integrator"] = self.integrator
        kwds["support_code_lines"] += GSL_support_code.split("\n")
        kwds["t_array"] = f"{self.get_array_name(self.variables['t'])}[0]"
        kwds["dt_array"] = f"{self.get_array_name(self.variables['dt'])}[0]"
        kwds["define_dt"] = "dt" not in variables_in_scalar
        kwds["cpp_standalone"] = self.is_cpp_standalone()
        for key, value in list(pointer_names.items()):
            kwds[key] = value
        return scalar_code, vector_code, kwds


class GSLCythonCodeGenerator(GSLCodeGenerator):
    syntax = {
        "end_statement": "",
        "access_pointer": ".",
        "start_declare": "cdef extern ",
        "open_function": ":",
        "open_struct": ":",
        "end_function": "",
        "end_struct": "",
        "open_cast": "<",
        "close_cast": ">",
        "diff_var_declaration": "",
    }

    def c_data_type(self, dtype):
        return c_data_type(dtype)

    def initialize_array(self, varname, values):
        value_list = ", ".join(repr(v) for v in values)
        code = "cdef double {varname}[{n_values}]\n"
        code += "{varname}[:] = [{value_list}]"
        return code.format(varname=varname, value_list=value_list, n_values=len(values))

    def var_replace_diff_var_lhs(self, var, ind):
        return {f"_gsl_{var}_f{ind}": f"f[{ind}]"}

    def var_init_lhs(self, var, type):
        return var

    def unpack_namespace_single(self, var_obj, in_vector, in_scalar):
        code = []
        if isinstance(var_obj, ArrayVariable):
            array_name = self.generator.get_array_name(var_obj)
            dtype = self.c_data_type(var_obj.dtype)
            if in_vector:
                code += [
                    f"_GSL_dataholder.{array_name} = <{dtype} *> _buf_{array_name}.data"
                ]
            if in_scalar:
                code += [f"{array_name} = <{dtype} *> _buf_{array_name}.data"]
        else:
            if in_vector:
                code += [
                    f'_GSL_dataholder.{var_obj.name} = _namespace["{var_obj.name}"]'
                ]
            if in_scalar:
                code += [f'{var_obj.name} = _namespace["{var_obj.name}"]']
        return "\n".join(code)

    @staticmethod
    def get_array_name(var, access_data=True):
        # We have to do the import here to avoid circular import dependencies.
        from brian2.codegen.generators.cython_generator import CythonCodeGenerator

        return CythonCodeGenerator.get_array_name(var, access_data)


class GSLCPPCodeGenerator(GSLCodeGenerator):
    def __getattr__(self, item):
        return getattr(self.generator, item)

    syntax = {
        "end_statement": ";",
        "access_pointer": "->",
        "start_declare": "",
        "open_function": "\n{",
        "open_struct": "\n{",
        "end_function": "\n}",
        "end_struct": "\n};",
        "open_cast": "(",
        "close_cast": ")",
        "diff_var_declaration": "const scalar ",
    }

    def c_data_type(self, dtype):
        return self.generator.c_data_type(dtype)

    def initialize_array(self, varname, values):
        value_list = ", ".join(repr(v) for v in values)
        return f"double const {varname}[] = {{{value_list}}};"

    def var_replace_diff_var_lhs(self, var, ind):
        scalar_dtype = self.c_data_type(prefs.core.default_float_dtype)
        f = f"f[{ind}]"
        try:
            if "unless refractory" in self.variable_flags[var]:
                return {
                    f"_gsl_{var}_f{ind}": f,
                    f"{scalar_dtype} _gsl_{var}_f{ind};": "",
                    f"{scalar_dtype} {f};": "",
                }  # in case the replacement of
                # _gsl_var_find to f[ind] happens
                # first
        except KeyError:
            pass
        return {f"const {scalar_dtype} _gsl_{var}_f{ind}": f}

    def var_init_lhs(self, var, type):
        return type + var

    def unpack_namespace_single(self, var_obj, in_vector, in_scalar):
        if isinstance(var_obj, ArrayVariable):
            pointer_name = self.get_array_name(var_obj, access_data=True)
            array_name = self.get_array_name(var_obj)
            if in_vector:
                return f"_GSL_dataholder.{pointer_name} = {array_name};"
            else:
                return ""
        else:
            if in_vector:
                return f"_GSL_dataholder.{var_obj.name} = {var_obj.name};"
            else:
                return ""

    def make_function_code(self, lines):
        # To pass constants that might be used in the function, we need to
        # add a %CONSTANTS% placeholder that will be replaced by the standalone
        # device
        return """
        // CONSTANTS
        %CONSTANTS%
        """ + super().make_function_code(lines)
