import sys
import traceback
import copy
import ctypes as c
import numpy as np
import os
import mpmath


def _print_error(msg_str):
    sys.stderr.write(msg_str + "\n")
    sys.stderr.flush()

try:
    _OpenBLASLibPath = os.path.join(os.path.dirname(os.path.abspath(__file__)),"OpenBLAS_install/lib/libopenblas.so.0")
    c.CDLL(_OpenBLASLibPath)
except OSError:
    _print_error("Warning: Failed to load OpenBLAS in its default place. "
                 "Excepting the library to be available from the system.")
_SpintrumLibPath = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib/libspintrum.so")
if not os.path.exists(_SpintrumLibPath):
    msg = "WARNING: Could not load the spintrum shared library in path " \
          + _SpintrumLibPath + ". This means that the installation is not valid " \
                               "and will most likely not work."
    _print_error(msg)
    print(msg)

mpmath.mp.dps = 50


def fft_spectral_density(data_list, sampling_rate):
    """
    Calculates the amplitude spectral density with both x and y axes
    :param data_list: Data, for which the FFT to be taken
    :param sampling_rate: the sample rate of the data
    :return: a dict with two elements, "x" for the x-axis (frequency axis),
             and "y" for the y-axis (the amplitude axis with units Amplitude/sqrt(Hz))
    """
    N = len(data_list)
    fft_data = np.fft.rfft(data_list)[0:int(N / 2 + 1)]
    bandwidth = np.double(sampling_rate / 2)
    scaling_factor = np.sqrt(1/bandwidth/np.double(N))
    x_axis = sampling_rate * np.array(range(int(N / 2 + 1))) / (np.double(N))
    abs_fft_data = scaling_factor*np.array(list(map(np.absolute,fft_data)))
    return {"x": x_axis, "y": abs_fft_data}


def FFTSpectralDensity(data_list, sampling_rate):
    """
    Deprecated. Please use fft_spectral_density()
    """
    return fft_spectral_density(data_list, sampling_rate)


def who_am_i():
    stack = traceback.extract_stack()
    filename, code_line, func_name, text = stack[-2]

    return func_name

def _raise_error(msg_str):
    sys.stderr.write(msg_str + "\n")
    raise Exception(msg_str)


def _is_var_type_correct(value, type_or_types):
    if type(type_or_types) is list:
        for tp in type_or_types:
            if type(value) is tp:
                return True
        return False

    else:
        if type(value) is type_or_types:
            return True
        else:
            return False


def _get_types_list_types_names(type_or_types):
    if type(type_or_types) is list:
        return str([tp.__name__ for tp in type_or_types])
    else:
        return str(type_or_types.__name__)


def _check_dict_elements_types(input_dict, types_dict):
    for val in input_dict:
        if val in types_dict:
            if _is_var_type_correct(input_dict[val], types_dict[val]) is False:
                _raise_error("Error: Type is not of expected type for variable: " + str(val) +
                             ". Possible allowed type(s) are: " + _get_types_list_types_names(types_dict[val]))


def _check_elements_exist_in_dict(input_dict, must_exist, can_exist):
    for el in must_exist:
        if el not in input_dict:
            _raise_error("Error: You should specify a value for: " + el)
    joined_elements = must_exist + can_exist

    for el in input_dict:
        if el not in joined_elements:
            _raise_error("Error: You have specified an unknown variable: " + el)


def _analyze_direction_string(dirs):
    activated_directions = {'x': 0, 'y': 0, 'z': 0, 'dirs': dirs}
    if (len(dirs) != 0) and ("x" not in dirs) and ("y" not in dirs) and ("z" not in dirs):
        _raise_error("Directions, for which trace has to be stored must be given as a string; eg x or xyz or xz, etc.")
    else:
        for c in dirs:
            if c == 'x':
                activated_directions['x'] = 1
            elif c == 'y':
                activated_directions['y'] = 1
            elif c == 'z':
                activated_directions['z'] = 1
    return activated_directions


class SpinOperations:
    _OPERATION_NAME_IDENTIFIER = '_OperationIdentifier'
    OPERATION__THERMAL_POPULATE = 0
    OPERATION__TIP_SPINS = 1
    OPERATION__SET_HAMILTONIAN = 2
    OPERATION__EVOLVE_TIME_INDEPENDENT = 3
    OPERATION__INIT_TIME_INDEPENDENT_EVOLUTION = 4
    OPERATION__EVOLVE_TIME_DEPENDENT = 5

    def __init__(self):
        self.operations_stack = None
        self.total_num_of_points = 0
        self.clear_operations()

    def clear_operations(self):
        self.operations_stack = []

    def add_operation(self, operation, parameters={}):
        if type(parameters) is not dict:
            _raise_error(who_am_i() + ": Error: parameters should be a dict.")

        if operation == SpinOperations.OPERATION__THERMAL_POPULATE:
            argument_types = {'Bx': [int, float], 'By': [int, float], 'Bz': [int, float],'T': [int,float]}
            must_exist_arguments = ['Bx', 'By', 'Bz','T']
            optional_arguments = []
            _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
            _check_dict_elements_types(parameters, argument_types)
            params_copy = copy.deepcopy(parameters)
            params_copy.update({SpinOperations._OPERATION_NAME_IDENTIFIER: operation})
            self.operations_stack.append(params_copy)

        elif operation == SpinOperations.OPERATION__TIP_SPINS:
            argument_types = {'direction': str, 'BVsTArea': [int, float]}
            must_exist_arguments = ['direction', 'BVsTArea']
            optional_arguments = []
            _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
            _check_dict_elements_types(parameters, argument_types)
            params_copy = copy.deepcopy(parameters)
            params_copy.update({SpinOperations._OPERATION_NAME_IDENTIFIER: operation})
            self.operations_stack.append(params_copy)

        elif operation == SpinOperations.OPERATION__SET_HAMILTONIAN:
            argument_types = {'Bx': [int, float], 'By': [int, float],
                              'Bz': [int, float], 'samplingRate': [int, float]}
            must_exist_arguments = []
            optional_arguments = ['Bx', 'By', 'Bz']
            _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
            _check_dict_elements_types(parameters, argument_types)
            params_copy = copy.deepcopy(parameters)
            params_copy.update({SpinOperations._OPERATION_NAME_IDENTIFIER: operation})
            self.operations_stack.append(params_copy)

        elif operation == SpinOperations.OPERATION__EVOLVE_TIME_INDEPENDENT:
            argument_types = {'points': int, 'threads': int}
            must_exist_arguments = ['points', 'threads']
            optional_arguments = []
            _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
            _check_dict_elements_types(parameters, argument_types)
            params_copy = copy.deepcopy(parameters)
            params_copy.update({SpinOperations._OPERATION_NAME_IDENTIFIER: operation})
            self.total_num_of_points += parameters['points']
            self.operations_stack.append(params_copy)

        elif operation == SpinOperations.OPERATION__INIT_TIME_INDEPENDENT_EVOLUTION:
            argument_types = {'samplingRate': [int, float], 'measurementDirection': str}
            must_exist_arguments = ['samplingRate', 'measurementDirection']
            optional_arguments = []
            _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
            _check_dict_elements_types(parameters, argument_types)
            params_copy = copy.deepcopy(parameters)
            params_copy.update({SpinOperations._OPERATION_NAME_IDENTIFIER: operation})
            self.operations_stack.append(params_copy)

        elif operation == SpinOperations.OPERATION__EVOLVE_TIME_DEPENDENT:
            argument_types = {'threads': int, 'input': dict, 'samplingRate': [int, float], 'store': str}
            must_exist_arguments = ['threads', 'input', 'samplingRate', ]
            optional_arguments = ['store']
            _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
            _check_dict_elements_types(parameters, argument_types)
            # use set to remove duplicates, and restore in dirs_to_store to get the reduced version
            # (removing duplicates preserves order by using key=parameters['store'].index)
            parameters['dirs_to_store'] = \
                _analyze_direction_string(''.join(sorted(set(parameters['store']), key=parameters['store'].index)))
            # total number of points = number of input values*number of directions to measure
            self.total_num_of_points += \
                len(list(parameters['input'].values())[0])*len(parameters['dirs_to_store']['dirs'])
            params_copy = copy.deepcopy(parameters)
            params_copy.update({SpinOperations._OPERATION_NAME_IDENTIFIER: operation})
            self.operations_stack.append(params_copy)

        else:
            _raise_error("Error: Operation Unknown!")


def _check_j_couplings_sanity(j_couplings):
    """
    raises an error if j couplings don't make sense
    :param j_couplings:
    :return:
    """
    if j_couplings is None: #no j-couplings provided
        return
    for i in range(len(j_couplings)):
        if len(j_couplings[i]) != len(j_couplings):
            _raise_error("Error: j-couplings must be a square matrix.")
        for j in range(len(j_couplings[i])):
            if j_couplings[i][j] != 0 and i >= j:
                _raise_error("Error: j-couplings must be an upper triangular matrix. "
                             "All other elements must be exactly 0.")


def simulate(**kwargs):
    argument_types = {'gyromagneticRatios': list, 'jCouplings': list, 'spinMultiplicities': list,
                      'spinOperations': SpinOperations}
    must_exist_arguments = ['gyromagneticRatios', 'spinOperations']
    optional_arguments = ['jCouplings','spinMultiplicities']
    _check_elements_exist_in_dict(kwargs, must_exist_arguments, optional_arguments)
    _check_dict_elements_types(kwargs, argument_types)

    if 'jCouplings' not in kwargs:
        kwargs['jCouplings'] = None
    if 'spinMultiplicities' not in kwargs:
        kwargs['spinMultiplicities'] = None

    _check_j_couplings_sanity(kwargs['jCouplings'])
    if kwargs['spinMultiplicities'] is not None and \
                    len(kwargs['gyromagneticRatios']) != len(kwargs['spinMultiplicities']):
        _raise_error("Spin multiplicities must have a length equal to the gyromagnetic ratios, "
                     "or alternatively, if you keep it empty, then it is assumed that all spins are 1/2")
    # print(kwargs)
    return _get_signal_c_call(kwargs)


class _CSpinOperation(c.Structure):
    _fields_ = [("OperationIdentifier",  c.c_long),
                ("Params_int_count",     c.c_long),
                ("Params_int",           c.POINTER(c.c_long)),
                ("Params_real_count",    c.c_long),
                ("Params_real",          c.POINTER(c.c_double)),
                ("Params_array_count",   c.c_long),
                ("Params_array_lengths", c.POINTER(c.c_long)),
                ("Params_array_names",   c.POINTER(c.POINTER(c.c_char))),
                ("Params_array",         c.POINTER(c.POINTER(c.c_double)))
                ]


def _direction_str_to_int(dir_str):
    if dir_str.lower() == 'x':
        return 0
    elif dir_str.lower() == 'y':
        return 1
    elif dir_str.lower() == 'z':
        return 2
    else:
        _raise_error("Direction of the tipping magnetic field can be either x, y or z. "
                     "You've entered \"" + dir_str + "\"")


def _create_c_operation(operation_id, int_params, real_params, array_params):

    # if int params exist, make them in a pointer to an array, otherwise, make it NULL (None)
    if len(int_params) > 0:
        int_params_c = (c.c_long * len(int_params))(*int_params) #convert list to c-array
        int_params_ptr = c.cast(int_params_c, c.POINTER(c.c_long)) #cast array to pointer
    else:
        int_params_ptr = None

    #if real params exist, make them in a pointer to an array, otherwise, make it NULL (None)
    if len(real_params) > 0:
        real_params_c = (c.c_double * len(real_params))(*real_params) #convert list to c-array
        real_params_ptr = c.cast(real_params_c, c.POINTER(c.c_double)) #cast array to pointer
    else:
        real_params_ptr = None

    if len(array_params) > 0:
        # arrays lengths
        array_lengths_list = list(map(len,array_params.values()))
        data_array_lengths_c = (c.c_long * len(array_params))(*array_lengths_list) #convert list to c-array
        data_array_lengths_ptr = c.cast(data_array_lengths_c, c.POINTER(c.c_long)) #cast array to pointer

        # arrays labels
        labels_array_type = (c.POINTER(c.c_char)*len(array_params))
        labels_c_array = labels_array_type()
        labels_keys_list = list(array_params.keys())
        for i in range(len(labels_keys_list)):
            labels_c_array[i] = c.create_string_buffer(str.encode(labels_keys_list[i]))

        # arrays data arrays
        arrays_array_type = (c.POINTER(c.c_double) * len(array_params))
        arrays_c_array = arrays_array_type()
        arrays_list = list(array_params.values())
        for i in range(len(arrays_list)):
            single_array_c_type = (c.c_double * len(arrays_list[i]))(*arrays_list[i])  # convert list to c-array
            single_array_c = single_array_c_type
            arrays_c_array[i] = c.cast(single_array_c, c.POINTER(c.c_double)) #cast array to pointer

    else:
        data_array_lengths_ptr = None
        labels_c_array = None
        arrays_c_array = None

    op_values = _CSpinOperation(operation_id,
                                len(int_params),  int_params_ptr,
                                len(real_params), real_params_ptr,
                                len(array_params),
                                data_array_lengths_ptr,
                                labels_c_array,
                                arrays_c_array)
    return c.byref(op_values)


def _get_operations_pointers(operations_stack):
    num_of_ops = len(operations_stack)
    structs_ptr_array_type = c.c_void_p * num_of_ops
    structs_ptr_array_obj = structs_ptr_array_type()
    structs_array = []
    for op in operations_stack:
        if op["_OperationIdentifier"] == SpinOperations.OPERATION__THERMAL_POPULATE:
            structs_array.append(_create_c_operation(op["_OperationIdentifier"], [], [op['Bx'],op['By'],op['Bz'],op['T']], {}))

        elif op["_OperationIdentifier"] == SpinOperations.OPERATION__TIP_SPINS:
            structs_array.append(_create_c_operation(op["_OperationIdentifier"],[_direction_str_to_int(op['direction'])], [op['BVsTArea']], {}))

        elif op["_OperationIdentifier"] == SpinOperations.OPERATION__SET_HAMILTONIAN:
            structs_array.append(_create_c_operation(op["_OperationIdentifier"], [], [op["Bx"], op["By"], op["Bz"]], {}))

        elif op["_OperationIdentifier"] == SpinOperations.OPERATION__EVOLVE_TIME_INDEPENDENT:
            structs_array.append(_create_c_operation(op["_OperationIdentifier"], [op["points"],op["threads"]], [], {}))

        elif op["_OperationIdentifier"] == SpinOperations.OPERATION__INIT_TIME_INDEPENDENT_EVOLUTION:
            structs_array.append(_create_c_operation(op["_OperationIdentifier"], [_direction_str_to_int(op['measurementDirection'])], [op["samplingRate"]], {}))

        elif op["_OperationIdentifier"] == SpinOperations.OPERATION__EVOLVE_TIME_DEPENDENT:
            structs_array.append(_create_c_operation(op["_OperationIdentifier"],
                                                     [op["threads"], op['dirs_to_store']['x'], op['dirs_to_store']['y'],
                                                      op['dirs_to_store']['z']], [op['samplingRate']], op["input"]))
    return structs_array


def _get_signal_c_call(parameters):
    structs_array = _get_operations_pointers(parameters['spinOperations'].operations_stack)
    operations_stack = parameters['spinOperations'].operations_stack
    num_of_ops = len(operations_stack)
    structs_ptr_array_type = c.c_void_p * num_of_ops
    structs_ptr_array_obj = structs_ptr_array_type()

    # print(structs_ptr_array_obj)

    # This next loop cannot be included in the function _get_operations_pointers
    # because taking c-pointers to structs_array elements doesn't make python preserve them, which causes them to
    # be deleted when exiting the function
    for i in range(len(structs_array)):
        structs_ptr_array_obj[i] = c.cast(structs_array[i], c.c_void_p)

    gammas = parameters['gyromagneticRatios']
    mults = parameters['spinMultiplicities']
    j_couplings = _convert_j_couplings_to_1d_array(parameters['jCouplings'])

    gammas_c_version = (c.c_double * len(gammas))(*gammas)
    gammas_ptr       = c.cast(gammas_c_version, c.POINTER(c.c_double * len(gammas)))

    if mults is None:
        mults_ptr = c.c_void_p(None)
    else:
        mults_c_version = (c.c_int * len(mults))(*mults)
        mults_ptr = c.cast(mults_c_version, c.POINTER(c.c_int * len(mults)))

    if j_couplings == []:
        j_couplings_ptr = c.c_void_p(None)
    else:
        j_couplings_c_version = (c.c_double * len(j_couplings))(*j_couplings)
        j_couplings_ptr = c.cast(j_couplings_c_version, c.POINTER(c.c_double * len(j_couplings)))

    mylib = c.CDLL(_SpintrumLibPath)

    num_of_points = parameters['spinOperations'].total_num_of_points

    return_array_type = c.c_double * num_of_points
    returned_array_ptr_type = c.POINTER(return_array_type)
    return_array = return_array_type()
    returned_array_ptr = c.byref(return_array)
    prototype = c.CFUNCTYPE(
        c.c_long, # return type
        c.c_void_p,
        c.c_void_p,
        c.c_void_p,
        c.c_void_p,
        c.c_int,
        c.c_int,
        c.c_long,
        returned_array_ptr_type
    )
    create_signal_function = prototype(('SimulateSignal', mylib))

    res = create_signal_function(structs_ptr_array_obj,
                                 gammas_ptr,
                                 j_couplings_ptr,
                                 mults_ptr,
                                 c.c_int(len(gammas)),
                                 c.c_int(len(structs_ptr_array_obj)),
                                 c.c_long(num_of_points),
                                 returned_array_ptr)
    # print(res)
    output = np.array(c.cast(returned_array_ptr,returned_array_ptr_type).contents)
    # print(output)
    return output


def _convert_j_couplings_to_1d_array(j_couplings):
    if j_couplings is None:
        return []

    output_array = []
    for i in range(len(j_couplings)):
        output_array += j_couplings[i][i + 1:len(j_couplings)]

    return output_array


def _get_derivative(params, param_index, fitted_function, epsilon=np.sqrt(np.finfo(float).eps)):
    """
    Calculates the derivatives of the fitted function (after having fitted a function) to some model
    :param params: list of parameter values at the point where the derivative has to be calculated
    :param param_index: index of the parameter, for which the derivative has to be taken
    :param fitted_function: the function, for which the derivative has to be calculated, where
           this function has to take "params" as an argument
    :param epsilon: the small step to calculate the numerical difference; the default value uses Python floats' epislon
    :return: the derivative value of the i-th parameter in params in the function fitted_function
    """
    h = params[param_index] * epsilon

    low_point__1 = np.delete(np.insert(params, param_index, params[param_index] - h), param_index + 1)
    high_point_1 = np.delete(np.insert(params, param_index, params[param_index] + h), param_index + 1)

    low_point__2 = np.delete(np.insert(params, param_index, params[param_index] - 2 * h), param_index + 1)
    high_point_2 = np.delete(np.insert(params, param_index, params[param_index] + 2 * h), param_index + 1)

    low_point__3 = np.delete(np.insert(params, param_index, params[param_index] - 3 * h), param_index + 1)
    high_point_3 = np.delete(np.insert(params, param_index, params[param_index] + 3 * h), param_index + 1)

    func_value_lp1 = fitted_function(low_point__1)
    func_value_hp1 = fitted_function(high_point_1)
    func_value_lp2 = fitted_function(low_point__2)
    func_value_hp2 = fitted_function(high_point_2)
    func_value_lp3 = fitted_function(low_point__3)
    func_value_hp3 = fitted_function(high_point_3)

    derivative = 1.5*(func_value_hp1['y'] - func_value_lp1['y'])/(2*h) - \
                 0.6*(func_value_hp2['y'] - func_value_lp2['y'])/(4*h) + \
                 0.1*(func_value_hp3['y'] - func_value_lp3['y'])/(6*h)

    return derivative


def get_correlation_matrix(optimum_params, fitted_function, objective_function):
    """
    A function that calculates the correlation matrix according to the book:
        Wolberg 2005: Data analysis using least-squares method
    :param optimum_params: list of optimum fitted parameters
    :param fitted_function: function that is fitted, that takes optimum_params as a parameter
    :param objective_function: the objective function used to fit. It should take optimum_params as parameter
    :return: the correlation matrix calculated
    """
    n = len(optimum_params) # number of parameters
    p = len(fitted_function(optimum_params)['x']) # number of fitted points
    min_obj_func_val = objective_function(optimum_params) # minimum objective_function value

    derivatives_matrix = \
        [_get_derivative(optimum_params, i, fitted_function).tolist() for i in range(len(optimum_params))]

    derivatives_matrix_high_precision = mpmath.matrix(derivatives_matrix)

    c_matrix = derivatives_matrix_high_precision*derivatives_matrix_high_precision.T

    factor = (min_obj_func_val**2/(p-n))
    correlation_matrix = (c_matrix**-1)*factor

    return correlation_matrix


def get_errorbars(matrix):
    error_bars = np.array([])
    for i in range(matrix.cols):
        error_bars = np.append(error_bars, np.float(np.sqrt(matrix[i,i])))
    return error_bars


class AtomicSpecies:
    GyromagneticRatio = None
    Name = None
    @staticmethod
    def _get_species_class_list():
        return AtomicSpecies.__subclasses__()

    @staticmethod
    def get_species_names():
        return list([cls.Name for cls in AtomicSpecies._get_species_class_list()])

    @staticmethod
    def get_species_gyromagnetic_ratios():
        return dict({cls.Name: cls.GyromagneticRatio for cls in AtomicSpecies._get_species_class_list()})

    @staticmethod
    def get_species_list_gyromagnetic_ratios(listOfNames):
        gyromagnetic_ratios = dict({cls.Name: cls.GyromagneticRatio for cls in AtomicSpecies._get_species_class_list()})
        output = []
        for atom in listOfNames:
            output.append(gyromagnetic_ratios[atom])
        return output

    def __init(self):
        pass


class AtomC13(AtomicSpecies):
    Name = "C13"
    GyromagneticRatio = 1070.8

    def __init__(self):
        AtomicSpecies.__init__(self)


class AtomH1(AtomicSpecies):
    Name = "H1"
    GyromagneticRatio = 4257.7

    def __init__(self):
        AtomicSpecies.__init__(self)


class SpinSimulator:
    def __init__(self, **kwargs):
        self._is_initialized = False
        self._spin_operations = None
        self.init(kwargs)

    def init(self,parameters):
        # pointer to object
        self.c_spin_ptr_to_object_type = c.c_void_p
        # pointer to pointer to object. Necessary to modify the pointer to object
        self.c_spin_ptr_to_ptr_type = c.POINTER(self.c_spin_ptr_to_object_type)
        self.c_spin_ptr_to_object = self.c_spin_ptr_to_object_type()
        self.c_spin_ptr_to_ptr = c.byref(self.c_spin_ptr_to_object)

        argument_types = {'gyromagneticRatios': list, 'jCouplings': list, 'doPrint': bool}

        must_exist_arguments = ['gyromagneticRatios']
        optional_arguments = ['jCouplings', 'spinMultiplicities', 'doPrint']
        _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
        _check_dict_elements_types(parameters, argument_types)

        if 'jCouplings' not in parameters:
            parameters['jCouplings'] = None
        if 'spinMultiplicities' not in parameters:
            parameters['spinMultiplicities'] = None

        _check_j_couplings_sanity(parameters['jCouplings'])
        if parameters['spinMultiplicities'] is not None and \
                        len(parameters['gyromagneticRatios']) != len(parameters['spinMultiplicities']):
            _raise_error("Spin multiplicities must have a length equal to the gyromagnetic ratios, "
                         "or alternatively, if you keep it empty, then it is assumed that all spins are 1/2")

        gammas = parameters['gyromagneticRatios']
        mults = parameters['spinMultiplicities']
        jCouplings = _convert_j_couplings_to_1d_array(parameters['jCouplings'])

        gammas_c_version = (c.c_double * len(gammas))(*gammas)
        gammas_ptr = c.cast(gammas_c_version, c.POINTER(c.c_double * len(gammas)))

        if mults is None:
            mults_ptr = c.c_void_p(None)
        else:
            mults_c_version = (c.c_int * len(mults))(*mults)
            mults_ptr = c.cast(mults_c_version, c.POINTER(c.c_int * len(mults)))

        if jCouplings == []:
            jCouplings_ptr = c.c_void_p(None)
        else:
            jCouplings_c_version = (c.c_double * len(jCouplings))(*jCouplings)
            jCouplings_ptr = c.cast(jCouplings_c_version, c.POINTER(c.c_double * len(jCouplings)))

        mylib = c.CDLL(_SpintrumLibPath)

        prototype = c.CFUNCTYPE(
            c.c_int,  # return type
            c.c_void_p,
            c.c_void_p,
            c.c_void_p,
            c.c_int,
            c.c_bool,
            self.c_spin_ptr_to_ptr_type
        )

        initSimulatorCreatorFunction = prototype(('InitializeSimulator', mylib))

        res = initSimulatorCreatorFunction(gammas_ptr,
                                          jCouplings_ptr,
                                          mults_ptr,
                                          c.c_int(len(gammas)),
                                          parameters['doPrint'],
                                          self.c_spin_ptr_to_ptr)

        if res >= 0:
            self._is_initialized = True
        else:
            self._is_initialized = False
            _raise_error("An error occurred while initializing the spin simulator. Possibly a memory issue.")
        return res

    def update_parameters(self,**parameters):
        argument_types = {'gyromagneticRatios': list, 'jCouplings': list,
                          'spinOperations': SpinOperations}

        must_exist_arguments = []
        optional_arguments = ['gyromagneticRatios',
                                'spinOperations', 'jCouplings']
        _check_elements_exist_in_dict(parameters, must_exist_arguments, optional_arguments)
        _check_dict_elements_types(parameters, argument_types)

        there_are_gammas = False
        there_are_j_couplings = False
        there_are_spin_operations = False

        if 'gyromagneticRatios' in parameters:
            gammas = parameters['gyromagneticRatios']
            gammas_c_version = (c.c_double * len(gammas))(*gammas)
            gammas_ptr = c.cast(gammas_c_version, c.POINTER(c.c_double * len(gammas)))
            there_are_gammas = True

        if 'jCouplings' in parameters:
            _check_j_couplings_sanity(parameters['jCouplings'])
            jCouplings = _convert_j_couplings_to_1d_array(parameters['jCouplings'])
            jCouplings_c_version = (c.c_double * len(jCouplings))(*jCouplings)
            jCouplings_ptr = c.cast(jCouplings_c_version, c.POINTER(c.c_double * len(jCouplings)))
            there_are_j_couplings = True

        if 'spinOperations' in parameters:
            spin_ops = parameters['spinOperations']
            there_are_spin_operations = True

        if there_are_gammas or there_are_j_couplings:
            mylib = c.CDLL(_SpintrumLibPath)

            prototype = c.CFUNCTYPE(
                c.c_int,  # return type
                c.c_void_p,
                c.c_void_p
            )

            #set gammas values
            if there_are_gammas:
                set_gammas_function = prototype(('SetGammasInSimulator', mylib))
                res = set_gammas_function(gammas_ptr,
                                          self.c_spin_ptr_to_object)

                if res >= 0:
                    pass
                else:
                    _raise_error("An error was found while trying to set gamma values.")


            # set j-couplings values
            if there_are_j_couplings:
                set_j_couplings_function = prototype(('SetJCouplingsInSimulator', mylib))
                res = set_j_couplings_function(jCouplings_ptr,
                                               self.c_spin_ptr_to_object)

                if res >= 0:
                    pass
                else:
                    _raise_error("An error was found while trying to set j-couplings values.")

        if there_are_spin_operations:
            self._spin_operations = spin_ops

    def simulate(self):
        if self._spin_operations is None:
            _raise_error("SpinOperations not initialized. Use update_parameters() and set spinOperations.")
        structs_array = _get_operations_pointers(self._spin_operations.operations_stack)
        num_of_ops = len(self._spin_operations.operations_stack)
        structs_ptr_array_type = c.c_void_p * num_of_ops
        structs_ptr_array_obj = structs_ptr_array_type()

        for i in range(len(structs_array)):
            structs_ptr_array_obj[i] = c.cast(structs_array[i], c.c_void_p)

        mylib = c.CDLL(_SpintrumLibPath)

        num_of_points = self._spin_operations.total_num_of_points

        returnArrayType = c.c_double * num_of_points
        returnedArrayPtrType = c.POINTER(returnArrayType)
        returnArray = returnArrayType()
        returnedArrayPtr = c.byref(returnArray)
        prototype = c.CFUNCTYPE(
            c.c_long, #return type
            self.c_spin_ptr_to_object_type,
            c.c_void_p,
            c.c_int,
            c.c_long,
            returnedArrayPtrType
        )
        createSpectFunction = prototype(('CreateSignalFromInitializedSimulator', mylib))

        res = createSpectFunction(self.c_spin_ptr_to_object,
                                  structs_ptr_array_obj,
                                  c.c_int(len(structs_ptr_array_obj)),
                                  c.c_long(num_of_points),
                                  returnedArrayPtr)
        # print(res)

        output = np.array(c.cast(returnedArrayPtr,returnedArrayPtrType).contents)
        # print(output)
        return output

    def __del__(self):
        # print("System initialization state: " + str(self._is_initialized))
        if self._is_initialized:
            mylib = c.CDLL(_SpintrumLibPath)

            prototype = c.CFUNCTYPE(
                c.c_int,  # return type
                c.c_void_p
            )
            deinitSimulatorFunction = prototype(('DenitializeSimulator', mylib))

            res = deinitSimulatorFunction(self.c_spin_ptr_to_object)
            if res >= 0:
                self._is_initialized = False
            else:
                _raise_error("An error was found while trying to free the memory.")
