"""
Copyright 2022 Maximilian Schaller
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import numpy as np
from datetime import datetime


def write_file(path, mode, function, *args):
    """Write data to a file using a specific utility function."""
    with open(path, mode) as file:
        function(file, *args)
    

def read_write_file(path, function, *args):
    """Read data from a file, process it, and write back."""
    with open(path, 'r') as file:
        data = file.read()
    data = function(data, *args)
    with open(path, 'w') as file:
        file.write(data)


def multiple_replace(text, replacements):
    """Perform multiple replacements (list of 2-tuples) on text"""
    for old, new in replacements:
        text = text.replace(old, new)
    return text


def cut_from_expr(text, expr):
    """Cut file contents from a specific expression"""
    idx = text.find(expr)
    
    if idx != -1:
        return text[:idx]
    else:
        return text


def write_vec_def(f, vec, name, typ):
    """
    Write vector to file
    """
    f.write(f'{typ} {name}[{len(vec)}] = {{\n')

    # Write vector components
    for i in range(len(vec)):
        if typ == 'cpg_float':
            f.write('(cpg_float)%.20f,\n' % vec[i])
        else:
            f.write(f'{vec[i]},\n')

    f.write('};\n')


def write_vec_prot(f, vec, name, typ):
    """
    Write vector to file
    """
    f.write(f'extern {typ} {name}[{len(vec)}];\n')


def write_mat_def(f, mat, name):
    """
    Write sparse matrix (scipy compressed sparse column) to file
    """
    write_vec_def(f, mat['i'], name + '_i', 'cpg_int')
    write_vec_def(f, mat['p'], name + '_p', 'cpg_int')
    write_vec_def(f, mat['x'], name + '_x', 'cpg_float')

    f.write(f'cpg_csc {name} = {{')
    f.write(f'{name}_p, ')
    f.write(f'{name}_i, ')
    f.write(f'{name}_x}};\n')


def write_mat_prot(f, mat, name):
    """
    Write sparse matrix (scipy compressed sparse column) to file
    """
    f.write(f'extern cpg_csc {name};\n')


def write_dense_mat_def(f, mat, name):
    """
    Write dense matrix to file
    """

    f.write(f'cpg_float {name}[{mat.size}] = {{\n')

    # represent matrix as vector (Fortran style)
    for j in range(mat.shape[1]):
        for i in range(mat.shape[0]):
            f.write('(cpg_float)%.20f,\n' % mat[i, j])

    f.write('};\n')


def write_dense_mat_prot(f, mat, name):
    """
    Write dense matrix to file
    """

    f.write(f'extern cpg_float cpg_{name}[{mat.size}];\n')


def write_description(f, file_type, content):
    """
    Timestamp and file content to beginning of file
    """

    if file_type == 'py':
        comment_str = '"""'
    else:
        comment_str = '/*'
    now = datetime.now()
    f.write(f'\n{comment_str}\n')
    f.write(f'Auto-generated by CVXPYgen {now.strftime("on %B %d, %Y at %H:%M:%S")}.\n')
    f.write(f'Content: {content}.\n')
    f.write(f'{comment_str[::-1]}\n\n')



def replace_in_file(filepath, replacements):
    """
    Replace strings in file
    """

    with open(filepath, 'r') as f:
        t = f.read()
    for old, new in replacements:
        t = t.replace(old, new)
    with open(filepath, 'w') as f:
        f.write(t)


def replace_inf(v):
    """
    Replace infinity by large number
    """

    # check if dealing with csc dict or numpy array
    if type(v) == dict:
        sign = np.sign(v['x'])
        idx = np.isinf(v['x'])
        v['x'][idx] = 1e30 * sign[idx]
    else:
        sign = np.sign(v)
        idx = np.isinf(v)
        v[idx] = 1e30 * sign[idx]

    return v


def zeros(n):
    """
    Return zero vector of length n
    """

    return np.zeros(n, dtype=float)


def csc_to_dict(m):
    """
    Convert scipy csc matrix to dict that can be passed to write_mat_def()
    """

    d = dict()
    d['i'] = m.indices
    d['p'] = m.indptr
    d['x'] = m.data

    return d


def param_is_empty(param):
    """
    Check if parameter is empty
    """

    if type(param) == dict:
        return param['x'].size == 0
    else:
        return param.size == 0


def is_mathematical_scalar(x):
    """
    Check if input is a scalar in mathematical sense, e.g., returning True for vectors with length 1
    """

    return True if np.isscalar(x) else x.size == 1


def write_problem_summary(name_to_shape, name_to_size):
    """
    Create html code for param / variables table entries
    """

    string = ''
    for n, sh in name_to_shape.items():
        if sh == ():
            shape_str = '1'
        elif len(sh) == 1:
            shape_str = str(sh[0])
        else:
            shape_str = f'{sh[0]} by {sh[1]} ({name_to_size[n]})'
        string += f'      <tr>\n        <td><code>{n}</code></td>\n        <td>{shape_str}</td>\n      </tr>\n'
    return string


def write_canonicalize_explicit(f, p_id, s, mapping, user_p_col_to_name_usp, user_p_name_to_size_usp, prefix):
    """
    Write function to compute canonical parameter value
    """

    sign_to_str = {1: '', -1: '-'}

    for row in range(len(mapping.indptr) - 1):
        expr = ''
        expr_is_const = True
        data = mapping.data[mapping.indptr[row]:mapping.indptr[row + 1]]
        columns = mapping.indices[mapping.indptr[row]:mapping.indptr[row + 1]]
        for (datum, col) in zip(data, columns):
            ex = '(%.20f)+' % datum
            for user_p_col, user_name in user_p_col_to_name_usp.items():
                if user_p_col + user_p_name_to_size_usp[user_name] > col:
                    expr_is_const = False
                    if user_p_name_to_size_usp[user_name] == 1:
                        if abs(datum) == 1:
                            ex = f'({sign_to_str[datum]}{prefix}CPG_Params.{user_name})+'
                        else:
                            ex = f'(%.20f*{prefix}CPG_Params.{user_name})+' % datum
                    else:
                        if abs(datum) == 1:
                            ex = f'({sign_to_str[datum]}{prefix}CPG_Params.{user_name}[%d])+' % (col - user_p_col)
                        else:
                            ex = f'(%.20f*{prefix}CPG_Params.{user_name}[%d])+' % (datum, col - user_p_col)
                    break
            expr += ex
        expr = expr[:-1]
        if data.size > 0 and not expr_is_const:
            if p_id == 'd':
                f.write(f'  {prefix}Canon_Params.d = {expr};\n')
            else:
                f.write(f'  {prefix}Canon_Params.{p_id}{s}[%d] = {expr};\n' % row)


def write_canonicalize(f, canon_name, s, mapping, prefix):
    """
    Write function to compute canonical parameter value
    """

    f.write(f'  for(i=0; i<{mapping.shape[0]}; i++){{\n')
    f.write(f'    {prefix}Canon_Params.{canon_name}{s}[i] = 0;\n')
    f.write(f'    for(j={prefix}canon_{canon_name}_map.p[i]; j<{prefix}canon_{canon_name}_map.p[i+1]; j++){{\n')
    f.write(f'      {prefix}Canon_Params.{canon_name}{s}[i] += {prefix}canon_{canon_name}_map.x[j]*{prefix}cpg_params_vec[{prefix}canon_{canon_name}_map.i[j]];\n')
    f.write(f'    }}\n')
    f.write(f'  }}\n')



def write_param_def(f, param, name, prefix, suffix):
    """
    Write vectors and matrices
    """
    if not param_is_empty(param):
        if name.isupper():
            write_mat_def(f, param, f'{prefix}canon_{name}{suffix}')
        elif name == 'd':
            f.write(f'cpg_float {prefix}canon_d{suffix} = %.20f;\n' % param[0])
        else:
            write_vec_def(f, param, f'{prefix}canon_{name}{suffix}', 'cpg_float')
        f.write('\n')


def write_param_prot(f, param, name, prefix, suffix):
    """
    Write vectors and matrices
    """
    if not param_is_empty(param):
        if name.isupper():
            write_mat_prot(f, param, f'{prefix}canon_{name}{suffix}')
        elif name == 'd':
            f.write(f'extern cpg_float {prefix}canon_d{suffix};\n')
        else:
            write_vec_prot(f, param, f'{prefix}canon_{name}{suffix}', 'cpg_float')


def write_struct_def(f, fields, casts, values, name, typ):
    """
    Write structure to file
    """

    f.write(f'{typ} {name} = {{\n')

    # write structure fields
    for field, cast, value in zip(fields, casts, values):
        if value in ['0', 'SCS_NULL']:
            cast = ''
        f.write(f'.{field} = {cast}{value},\n')

    f.write('};\n')


def write_struct_prot(f, name, typ):
    """
    Write structure to file
    """

    f.write(f'extern {typ} {name};\n')


def extend_functions_if_false(pus, functions_if_false):
    """
    Recursively extends functions_if_false in the parameter update structure
    """

    if functions_if_false is None:
        return []

    extended_functions_if_false = []

    for function in functions_if_false:
        extended_functions_if_false.append(function)
        extended_functions_if_false.extend(extend_functions_if_false(pus, pus[function].update_pending_logic.functions_if_false))

    return extended_functions_if_false


def remove_function(functions, function_to_remove):
    if function_to_remove in functions:
        functions.remove(function_to_remove)
    return functions


def analyze_pus(pus, p_id_to_changes):
    '''
    Analyze parameter update structure (pus) to return set of canonical update functions
    that are to be written to code without logical inter-connections and functions that
    are never called
    '''
    
    functions = set(pus.keys())
    functions_called = set(pus.keys())
    functions_secondary = []
    
    for function, logic in pus.items():
        
        up_logic = logic.update_pending_logic
        operator = up_logic.operator
        functions_if_false = extend_functions_if_false(pus, up_logic.functions_if_false)
            
        if operator in ['&&', '&', 'and', 'AND']:
            skip = False
            for p in up_logic.parameters_outdated:
                if not p_id_to_changes.get(p, False):
                    functions_called = remove_function(functions_called, function)
                    skip = True
            if skip:
                continue
        elif operator in ['||', '|', 'or', 'OR']:
            skip = True
            for p in up_logic.parameters_outdated:
                if p_id_to_changes.get(p, False):
                    skip = False
            if skip:
                functions_called = remove_function(functions_called, function)
                continue
        elif operator is None:
            if up_logic.extra_condition_operator is None and len(up_logic.parameters_outdated) == 1 and not p_id_to_changes[function]:
                functions_called = remove_function(functions_called, function)
                continue
        else:
            raise ValueError(f'Operator "{operator}" not implemented.')
        
        if functions_if_false is None:
            continue
            
        for f in functions_if_false:
            if f not in functions:
                raise ValueError(f'"{f}" is not part of parameter update structure.')
        functions_secondary.extend(functions_if_false)
                    
    return functions_called-set(functions_secondary), functions-functions_called


operator_map = {'&&': '&&', '&': '&&', 'and': '&&', 'AND': '&&',
                '||': '||', '|': '||', 'or': '||', 'OR': '||'}


def write_update_structure(f, configuration, parameter_canon, pus, functions, functions_never_called, depth=0):
    """
    Recursively write logical parameter update structure to file
    """

    if functions is None:
        return

    write_else = depth > 0 and len(set(functions) - set(functions_never_called)) > 0

    if write_else:
        f.write(' else {\n')

    for function in functions:

        logic = pus[function]
        up_logic = logic.update_pending_logic
        if 'P' in up_logic.parameters_outdated and not parameter_canon.quad_obj:
            up_logic.parameters_outdated.remove('P')

        if function not in functions_never_called:
            extra_condition = f'{up_logic.extra_condition.format(prefix=configuration.prefix)} ' if up_logic.extra_condition is not None else ''
            extra_condition_operator = f' {operator_map.get(up_logic.extra_condition_operator, "||")} ' if up_logic.extra_condition is not None and len(up_logic.parameters_outdated) > 0 else ''
            parameters_outdated = f'{(" "+operator_map.get(up_logic.operator, "&&")+" ").join([f"{configuration.prefix}Canon_Outdated.{p}" for p in up_logic.parameters_outdated])}'

            f.write(f'{"  "*(depth+1)}if ({extra_condition}{extra_condition_operator}{parameters_outdated}) {{\n')
            f.write(f'{"  "*(depth+2)}{logic.function_call.format(prefix=configuration.prefix)};\n')
            f.write(f'{"  "*(depth+1)}}}')
            new_depth = depth + 1
        else:
            new_depth = depth * 1

        write_update_structure(f, configuration, parameter_canon, pus, up_logic.functions_if_false, functions_never_called, new_depth)

        if function not in functions_never_called:
            f.write('\n')

    if write_else:
        f.write(f'{"  " * depth}}}')



def write_workspace_def(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface):

    write_description(f, 'c', 'Variable definitions')
    f.write('#include "cpg_workspace.h"\n')

    if configuration.unroll:
        f.write('\n// User-defined parameters\n')
        user_casts = []
        user_values = []
        names = list(parameter_info.writable.keys())
        for name in names:
            value = parameter_info.writable[name]
            if is_mathematical_scalar(value):
                user_casts.append('')
                user_values.append('%.20f' % value)
            else:
                write_vec_def(f, value, f'{configuration.prefix}cpg_{name}', 'cpg_float')
                f.write('\n')
                user_casts.append('(cpg_float *) ')
                user_values.append(f'&{configuration.prefix}cpg_{name}')
        f.write('// Struct containing all user-defined parameters\n')
        write_struct_def(f, names, user_casts, user_values, f'{configuration.prefix}CPG_Params', 'CPG_Params_t')
        f.write('\n')
    else:
        f.write('\n// Vector containing flattened user-defined parameters\n')
        write_vec_def(f, parameter_info.flat_usp, f'{configuration.prefix}cpg_params_vec', 'cpg_float')
        f.write('\n// Sparse mappings from user-defined to canonical parameters\n')
        for p_id, mapping in parameter_canon.p_id_to_mapping.items():
            if parameter_canon.p_id_to_changes[p_id]:
                write_mat_def(f, csc_to_dict(mapping), f'{configuration.prefix}canon_{p_id}_map')
                f.write('\n')

    p_ids = list(parameter_canon.p.keys())
    canon_casts = []
    f.write('// Canonical parameters\n')
    for p_id in p_ids:
        p = parameter_canon.p[p_id]
        if p_id == 'd':
            canon_casts.append('')
        else:
            write_param_def(f, replace_inf(p), p_id, configuration.prefix, '')
            if solver_interface.inmemory_preconditioning:
                write_param_def(f, replace_inf(p), p_id, configuration.prefix, '_conditioning')
            if p_id.isupper():
                canon_casts.append('')
            else:
                canon_casts.append('(cpg_float *) ')

    f.write('// Struct containing canonical parameters\n')

    struct_values = []
    struct_values_conditioning = []
    for i, p_id in enumerate(p_ids):
        p = parameter_canon.p[p_id]
        if type(p) == dict:
            length = len(p['x'])
        else:
            length = len(p)
        if length == 0:
            struct_values.append('0')
            if solver_interface.inmemory_preconditioning:
                struct_values_conditioning.append('0')
        elif p_id=='d':
            struct_values.append('%.20f' % p)
            if solver_interface.inmemory_preconditioning:
                struct_values_conditioning.append('%.20f' % p)
        else:
            struct_values.append(f'&{configuration.prefix}canon_{p_id}')
            if solver_interface.inmemory_preconditioning:
                struct_values_conditioning.append(f'&{configuration.prefix}canon_{p_id}_conditioning')

    write_struct_def(f, p_ids, canon_casts, struct_values, f'{configuration.prefix}Canon_Params', 'Canon_Params_t')
    f.write('\n')
    if solver_interface.inmemory_preconditioning:
        write_struct_def(f, p_ids, canon_casts, struct_values_conditioning, f'{configuration.prefix}Canon_Params_conditioning', 'Canon_Params_t')
        f.write('\n')

    # Boolean struct for outdated parameter flags
    f.write('// Struct containing flags for outdated canonical parameters\n')
    f.write(f'Canon_Outdated_t {configuration.prefix}Canon_Outdated = {{\n')
    for p_id in parameter_canon.p.keys():
        f.write(f'.{p_id} = 0,\n')
    f.write('};\n\n')

    prim_cast = []
    if any(variable_info.name_to_sym) or not solver_interface.sol_statically_allocated:
        f.write('// User-defined variables\n')
    for name, value in variable_info.name_to_init.items():
        if is_mathematical_scalar(value):
            prim_cast.append('')
        else:
            prim_cast.append('(cpg_float *) ')
            if variable_info.name_to_sym[name] or not solver_interface.sol_statically_allocated:
                write_vec_def(f, value.flatten(order='F'), configuration.prefix + name, 'cpg_float')
                f.write('\n')

    result_prefix = configuration.prefix if not solver_interface.ws_statically_allocated_in_solver_code else ''

    f.write('// Struct containing primal solution\n')
    CPG_Prim_fields = list(variable_info.name_to_init.keys())
    CPG_Prim_values = []
    for name, var in variable_info.name_to_init.items():
        offset = variable_info.name_to_offset[name]
        if is_mathematical_scalar(var):
            CPG_Prim_values.append('0')
        else:
            if variable_info.name_to_sym[name] or not solver_interface.sol_statically_allocated:
                CPG_Prim_values.append('&' + configuration.prefix + name)
            else:
                CPG_Prim_values.append(f'&{result_prefix}{solver_interface.ws_ptrs.primal_solution} + {offset}')
    write_struct_def(f, CPG_Prim_fields, prim_cast, CPG_Prim_values, f'{configuration.prefix}CPG_Prim', 'CPG_Prim_t')

    if len(dual_variable_info.name_to_init) > 0:
        dual_cast = []
        if not solver_interface.sol_statically_allocated:
            f.write('\n// Dual variables associated with user-defined constraints\n')
        for name, value in dual_variable_info.name_to_init.items():
            if is_mathematical_scalar(value):
                dual_cast.append('')
            else:
                dual_cast.append('(cpg_float *) ')
                if not solver_interface.sol_statically_allocated:
                    write_vec_def(f, value.flatten(order='F'), configuration.prefix + name, 'cpg_float')
                    f.write('\n')

        f.write('// Struct containing dual solution\n')
        CPG_Dual_fields = dual_variable_info.name_to_init.keys()
        CPG_Dual_values = []
        for name, var in dual_variable_info.name_to_init.items():
            vec = dual_variable_info.name_to_vec[name]
            offset = dual_variable_info.name_to_offset[name]
            if is_mathematical_scalar(var):
                CPG_Dual_values.append('0')
            else:
                if not solver_interface.sol_statically_allocated:
                    CPG_Dual_values.append('&' + configuration.prefix + name)
                else:
                    CPG_Dual_values.append(f'&{result_prefix}{solver_interface.ws_ptrs.dual_solution.format(dual_var_name=vec)} + {offset}')
        write_struct_def(f, CPG_Dual_fields, dual_cast, CPG_Dual_values, f'{configuration.prefix}CPG_Dual', 'CPG_Dual_t')

    f.write('\n// Struct containing solver info\n')
    CPG_Info_fields = ['obj_val', 'iter', 'status', 'pri_res', 'dua_res']
    CPG_Info_values = ['0', '0', ('0' if solver_interface.status_is_int else '"unknown"'), '0', '0']
    info_cast = ['', '', '', '', '']
    write_struct_def(f, CPG_Info_fields, info_cast, CPG_Info_values, f'{configuration.prefix}CPG_Info', 'CPG_Info_t')

    f.write('\n// Struct containing solution and info\n')
    if len(dual_variable_info.name_to_init) > 0:
        CPG_Result_fields = ['prim', 'dual', 'info']
        result_cast = ['', '', '']
        CPG_Result_values = [f'&{configuration.prefix}CPG_Prim', f'&{configuration.prefix}CPG_Dual',
                            f'&{configuration.prefix}CPG_Info']
    else:
        CPG_Result_fields = ['prim', 'info']
        result_cast = ['', '']
        CPG_Result_values = [f'&{configuration.prefix}CPG_Prim', f'&{configuration.prefix}CPG_Info']
    write_struct_def(f, CPG_Result_fields, result_cast, CPG_Result_values, f'{configuration.prefix}CPG_Result', 'CPG_Result_t')

    if solver_interface.stgs_requires_extra_struct_type:
        f.write('\n// Struct containing solver settings\n')
        f.write(f'Canon_Settings_t {configuration.prefix}Canon_Settings = {{\n')
        for name, default in solver_interface.stgs_names_to_default.items():
            f.write(f'.{name} = {default},\n')
        f.write('};\n')

    if not solver_interface.ws_statically_allocated_in_solver_code:
        solver_interface.define_workspace(f, configuration.prefix, parameter_canon)
        
    if configuration.gradient:
        
        zeros_N = zeros(solver_interface.n_var + solver_interface.n_eq + solver_interface.n_ineq)
        f.write('\n// Derivative workspace\n')
        write_vec_def(f, zeros(solver_interface.n_var), f'{configuration.prefix}cpg_canon_delta_x', 'cpg_float')
        write_vec_def(f, zeros_N, f'{configuration.prefix}cpg_canon_gradient_d', 'cpg_float')
        write_vec_def(f, zeros_N, f'{configuration.prefix}cpg_canon_gradient_rhs', 'cpg_float')
        write_vec_def(f, zeros_N, f'{configuration.prefix}cpg_canon_gradient_r', 'cpg_float')
        write_vec_def(f, zeros(solver_interface.n_var + 2*solver_interface.n_eq + solver_interface.n_ineq),
                      f'{configuration.prefix}cpg_canon_gradient_dqlu', 'cpg_float')
        write_vec_def(f, zeros(parameter_canon.p_csc['P'].nnz), f'{configuration.prefix}cpg_canon_gradient_dP', 'cpg_float')
        write_vec_def(f, zeros(parameter_canon.p_csc['A'].nnz), f'{configuration.prefix}cpg_canon_gradient_dA', 'cpg_float')
        write_vec_def(f, zeros(len(parameter_info.flat_usp)), f'{configuration.prefix}cpg_delta_p', 'cpg_float')
        
        delta_cast = []
        f.write('// User-defined parameter deltas\n')
        for col, name in parameter_info.col_to_name_usp.items():
            if parameter_info.name_to_size_usp[name] == 1:
                delta_cast.append('')
            else:
                delta_cast.append('(cpg_float *) ')
        
        f.write('\n// Struct containing parameter deltas\n')
        CPG_Delta_fields = list(parameter_info.col_to_name_usp.values())
        CPG_Delta_values = []
        for col, name in parameter_info.col_to_name_usp.items():
            if parameter_info.name_to_size_usp[name] == 1:
                CPG_Delta_values.append('0')
            else:
                CPG_Delta_values.append(f'&{configuration.prefix}cpg_delta_p + {col}')
        write_struct_def(f, CPG_Delta_fields, delta_cast, CPG_Delta_values, f'{configuration.prefix}CPG_Delta', 'CPG_Delta_t')


def write_workspace_prot(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface):
    """"
    Write workspace initialization to file
    """

    write_description(f, 'c', 'Type definitions and variable declarations')
    for header_file in solver_interface.header_files:
        f.write(f'#include {header_file}\n')

    # definition safeguard
    f.write('\n#ifndef CPG_TYPES_H\n')
    f.write('# define CPG_TYPES_H\n\n')

    f.write(f'typedef {solver_interface.numeric_types["float"]} cpg_float;\n')
    f.write(f'typedef {solver_interface.numeric_types["int"]} cpg_int;\n\n')

    # struct definitions
    f.write('// Compressed sparse column matrix\n')
    f.write('typedef struct {\n')
    f.write('  cpg_int      *p;\n')
    f.write('  cpg_int      *i;\n')
    f.write('  cpg_float    *x;\n')
    f.write('} cpg_csc;\n\n')

    if configuration.unroll:
        f.write('// User-defined parameters\n')
        f.write('typedef struct {\n')
        # single user parameters
        for name, size in parameter_info.name_to_size_usp.items():
            if size == 1:
                s = ''
            else:
                s = '*'
            f.write(f'  cpg_float    {(s+name+";").ljust(9)}   // Your parameter {name}\n')
        f.write('} CPG_Params_t;\n\n')

    f.write('// Canonical parameters\n')
    f.write('typedef struct {\n')
    for p_id in parameter_canon.p.keys():
        if p_id.isupper():
            f.write(f'  cpg_csc        *{(p_id+";").ljust(8)}   // Canonical parameter {p_id}\n')
        else:
            if p_id == 'd':
                s = ''
            else:
                s = '*'
            f.write(f'  cpg_float    {(s+p_id+";").ljust(9)}   // Canonical parameter {p_id}\n')
    f.write('} Canon_Params_t;\n\n')

    f.write('// Flags indicating outdated canonical parameters\n')
    f.write('typedef struct {\n')
    for p_id in parameter_canon.p.keys():
        f.write(f'  int        {(p_id + ";").ljust(8)}    // Bool, if canonical parameter {p_id} outdated\n')
    f.write('} Canon_Outdated_t;\n\n')

    f.write('// Primal solution\n')
    f.write('typedef struct {\n')
    for name, var in variable_info.name_to_init.items():
        if is_mathematical_scalar(var):
            s = ''
        else:
            s = '*'
        f.write(f'  cpg_float    {(s + name + ";").ljust(9)}   // Your variable {name}\n')
    f.write('} CPG_Prim_t;\n\n')

    if len(dual_variable_info.name_to_init) > 0:
        f.write('// Dual solution\n')
        f.write('typedef struct {\n')
        for name, var in dual_variable_info.name_to_init.items():
            if is_mathematical_scalar(var):
                s = ''
            else:
                s = '*'
            f.write(f'  cpg_float    {(s + name + ";").ljust(9)}   // Your dual variable for constraint {name}\n')
        f.write('} CPG_Dual_t;\n\n')

    f.write('// Solver information\n')
    f.write('typedef struct {\n')
    f.write('  cpg_float    obj_val;    // Objective function value\n')
    f.write('  cpg_int      iter;       // Number of iterations\n')
    f.write(f'  {"cpg_int      " if solver_interface.status_is_int else "char       *"}status;     // Solver status\n')
    f.write('  cpg_float    pri_res;    // Primal residual\n')
    f.write('  cpg_float    dua_res;    // Dual residual\n')
    f.write('} CPG_Info_t;\n\n')

    f.write('// Solution and solver information\n')
    f.write('typedef struct {\n')
    f.write('  CPG_Prim_t *prim;      // Primal solution\n')
    if len(dual_variable_info.name_to_init) > 0:
        f.write('  CPG_Dual_t *dual;      // Dual solution\n')
    f.write('  CPG_Info_t *info;      // Solver info\n')
    f.write('} CPG_Result_t;\n\n')
    
    f.write('// Parameter deltas\n')
    f.write('typedef struct {\n')
    for name, size in parameter_info.name_to_size_usp.items():
        if size == 1:
            s = ''
        else:
            s = '*'
        f.write(f'  cpg_float    {(s + name + ";").ljust(9)}   // Delta of your parameter {name}\n')
    f.write('} CPG_Delta_t;\n\n')

    if solver_interface.stgs_requires_extra_struct_type:
        f.write('// Solver settings\n')
        f.write('typedef struct {\n')
        for name, typ in solver_interface.stgs_names_to_type.items():
            f.write(f'  {typ.ljust(11)}{name};\n')
        f.write('} Canon_Settings_t;\n\n')

    f.write('#endif // ifndef CPG_TYPES_H\n')

    if configuration.unroll:
        f.write('\n// User-defined parameters\n')
        for name, value in parameter_info.writable.items():
            if not is_mathematical_scalar(value):
                write_vec_prot(f, value, f'{configuration.prefix}cpg_{name}', 'cpg_float')
        f.write('\n// Struct containing all user-defined parameters\n')
        write_struct_prot(f, f'{configuration.prefix}CPG_Params', 'CPG_Params_t')
    else:
        f.write('\n// Vector containing flattened user-defined parameters\n')
        write_vec_prot(f, parameter_info.flat_usp, f'{configuration.prefix}cpg_params_vec', 'cpg_float')
        f.write('\n// Sparse mappings from user-defined to canonical parameters\n')
        for p_id, mapping in parameter_canon.p_id_to_mapping.items():
            if parameter_canon.p_id_to_changes[p_id]:
                write_mat_prot(f, csc_to_dict(mapping), f'{configuration.prefix}canon_{p_id}_map')

    f.write('\n// Canonical parameters\n')
    for p_id, p in parameter_canon.p.items():
        if p_id != 'd':
            write_param_prot(f, p, p_id, f'{configuration.prefix}', '')
            if solver_interface.inmemory_preconditioning:
                write_param_prot(f, p, p_id, f'{configuration.prefix}', '_conditioning')

    f.write('\n// Struct containing canonical parameters\n')
    write_struct_prot(f, f'{configuration.prefix}Canon_Params', 'Canon_Params_t')
    if solver_interface.inmemory_preconditioning:
        write_struct_prot(f, f'{configuration.prefix}Canon_Params_conditioning', 'Canon_Params_t')

    f.write('\n// Struct containing flags for outdated canonical parameters\n')
    f.write(f'extern Canon_Outdated_t {configuration.prefix}Canon_Outdated;\n')

    if any(variable_info.name_to_sym.values()) or not solver_interface.sol_statically_allocated:
        f.write('\n// User-defined variables\n')
        for name, value in variable_info.name_to_init.items():
            if variable_info.name_to_sym[name] or not solver_interface.sol_statically_allocated:
                if not is_mathematical_scalar(value):
                    write_vec_prot(f, value.flatten(order='F'), f'{configuration.prefix}cpg_{name}', 'cpg_float')

    if not solver_interface.sol_statically_allocated:
        f.write('\n// Dual variables associated with user-defined constraints\n')
        for name, value in dual_variable_info.name_to_init.items():
            if not is_mathematical_scalar(value):
                write_vec_prot(f, value.flatten(order='F'), f'{configuration.prefix}cpg_{name}', 'cpg_float')

    f.write('\n// Struct containing primal solution\n')
    write_struct_prot(f, f'{configuration.prefix}CPG_Prim', 'CPG_Prim_t')

    if len(dual_variable_info.name_to_init) > 0:
        f.write('\n// Struct containing dual solution\n')
        write_struct_prot(f, f'{configuration.prefix}CPG_Dual', 'CPG_Dual_t')

    f.write('\n// Struct containing solver info\n')
    write_struct_prot(f, f'{configuration.prefix}CPG_Info', 'CPG_Info_t')        

    f.write('\n// Struct containing solution and info\n')
    write_struct_prot(f, f'{configuration.prefix}CPG_Result', 'CPG_Result_t')

    if solver_interface.stgs_requires_extra_struct_type:
        f.write('\n// Struct containing solver settings\n')
        write_struct_prot(f, f'{configuration.prefix}Canon_Settings', 'Canon_Settings_t')

    if not solver_interface.ws_statically_allocated_in_solver_code:
        solver_interface.declare_workspace(f, configuration.prefix, parameter_canon)
        
    if configuration.gradient:
        
        zeros_N = zeros(solver_interface.n_var + solver_interface.n_eq + solver_interface.n_ineq)
        f.write('\n// Derivative workspace\n')
        write_vec_prot(f, zeros(solver_interface.n_var), f'{configuration.prefix}cpg_canon_delta_x', 'cpg_float')
        write_vec_prot(f, zeros_N, f'{configuration.prefix}cpg_canon_gradient_d', 'cpg_float')
        write_vec_prot(f, zeros_N, f'{configuration.prefix}cpg_canon_gradient_rhs', 'cpg_float')
        write_vec_prot(f, zeros_N, f'{configuration.prefix}cpg_canon_gradient_r', 'cpg_float')
        write_vec_prot(f,  zeros(solver_interface.n_var + 2*solver_interface.n_eq + solver_interface.n_ineq),
                       f'{configuration.prefix}cpg_canon_gradient_dqlu', 'cpg_float')
        write_vec_prot(f, zeros(parameter_canon.p_csc['P'].nnz), f'{configuration.prefix}cpg_canon_gradient_dP', 'cpg_float')
        write_vec_prot(f, zeros(parameter_canon.p_csc['A'].nnz), f'{configuration.prefix}cpg_canon_gradient_dA', 'cpg_float')
        write_vec_prot(f, zeros(len(parameter_info.flat_usp)), f'{configuration.prefix}cpg_delta_p', 'cpg_float')
        
        f.write('\n// Struct containing parameter deltas\n')
        write_struct_prot(f, f'{configuration.prefix}CPG_Delta', 'CPG_Delta_t')


def write_solve_def(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface):
    """
    Write parameter initialization function to file
    """

    write_description(f, 'c', 'Function definitions')
    f.write('#include "cpg_solve.h"\n')
    f.write('#include "cpg_workspace.h"\n\n')

    if not configuration.unroll:
        f.write('static cpg_int i;\n')
        f.write('static cpg_int j;\n')

    if configuration.unroll and solver_interface.inmemory_preconditioning:
        f.write('static cpg_int i;\n')

    f.write('\n// Update user-defined parameters\n')
    if configuration.unroll:
        for user_p_name, Canon_outdated_names in parameter_canon.user_p_name_to_canon_outdated.items():
            if parameter_info.name_to_size_usp[user_p_name] == 1:
                f.write(f'void {configuration.prefix}cpg_update_{user_p_name}(cpg_float val){{\n')
                f.write(f'  {configuration.prefix}CPG_Params.{user_p_name} = val;\n')
            else:
                f.write(f'void {configuration.prefix}cpg_update_{user_p_name}(cpg_int idx, cpg_float val){{\n')
                f.write(f'  {configuration.prefix}CPG_Params.{user_p_name}[idx] = val;\n')
            for Canon_outdated_name in Canon_outdated_names:
                f.write(f'  {configuration.prefix}Canon_Outdated.{Canon_outdated_name} = 1;\n')
            f.write('}\n\n')
    else:
        for base_col, name in parameter_info.col_to_name_usp.items():
            Canon_outdated_names = parameter_canon.user_p_name_to_canon_outdated[name]
            if parameter_info.name_to_size_usp[name] == 1:
                f.write(f'void {configuration.prefix}cpg_update_{name}(cpg_float val){{\n')
                f.write(f'  {configuration.prefix}cpg_params_vec[%d] = val;\n' % base_col)
            else:
                f.write(f'void {configuration.prefix}cpg_update_{name}(cpg_int idx, cpg_float val){{\n')
                f.write(f'  {configuration.prefix}cpg_params_vec[idx+{base_col}] = val;\n')
            for Canon_outdated_name in Canon_outdated_names:
                f.write(f'  {configuration.prefix}Canon_Outdated.{Canon_outdated_name} = 1;\n')
            f.write('}\n\n')

    f.write('// Map user-defined to canonical parameters\n')

    for p_id, mapping in parameter_canon.p_id_to_mapping.items():
        if parameter_canon.p_id_to_changes[p_id]:
            f.write(f'void {configuration.prefix}cpg_canonicalize_{p_id}(){{\n')
            s = '->x' if p_id.isupper() else ''
            if configuration.unroll:
                write_canonicalize_explicit(f, p_id, s, mapping, parameter_info.col_to_name_usp,
                                            parameter_info.name_to_size_usp, configuration.prefix)
            else:
                write_canonicalize(f, p_id, s, mapping, configuration.prefix)
            f.write('}\n\n')

    result_prefix = configuration.prefix if not solver_interface.ws_statically_allocated_in_solver_code else ''
    prim_str = result_prefix + solver_interface.ws_ptrs.primal_solution
    dual_str = result_prefix + solver_interface.ws_ptrs.dual_solution

    if solver_interface.ret_prim_func_exists(variable_info):
        f.write('// Retrieve primal solution in terms of user-defined variables\n')
        f.write(f'void {configuration.prefix}cpg_retrieve_prim(){{\n')
        for var_name, indices in variable_info.name_to_indices.items():
            if len(indices) == 1:
                f.write(f'  {configuration.prefix}CPG_Prim.{var_name} = {prim_str}[%d];\n' % indices[0])
            elif variable_info.name_to_sym[var_name] or not solver_interface.sol_statically_allocated:
                for i, idx in enumerate(indices):
                    f.write(f'  {configuration.prefix}CPG_Prim.{var_name}[%d] = {prim_str}[%d];\n' % (i, idx))
        f.write('}\n\n')

    if solver_interface.ret_dual_func_exists(dual_variable_info):
        f.write('// Retrieve dual solution in terms of user-defined constraints\n')
        f.write(f'void {configuration.prefix}cpg_retrieve_dual(){{\n')
        for var_name, (canonical_var_name, indices) in dual_variable_info.name_to_indices.items():
            if len(indices) == 1:
                f.write(f'  {configuration.prefix}CPG_Dual.{var_name} = {dual_str.format(dual_var_name=canonical_var_name)}[%d];\n' % indices[0])
            elif not solver_interface.sol_statically_allocated:
                for i, idx in enumerate(indices):
                    f.write(f'  {configuration.prefix}CPG_Dual.{var_name}[%d] = {dual_str.format(dual_var_name=canonical_var_name)}[%d];\n' % (i, idx))
        f.write('}\n\n')

    f.write('// Retrieve solver info\n')
    f.write(f'void {configuration.prefix}cpg_retrieve_info(){{\n')
    f.write(f'  {configuration.prefix}CPG_Info.obj_val = {"-" if parameter_canon.is_maximization else ""}({result_prefix}{solver_interface.ws_ptrs.objective_value}{" + " + configuration.prefix + "Canon_Params.d" if parameter_canon.nonzero_d else ""});\n')
    f.write(f'  {configuration.prefix}CPG_Info.iter = {result_prefix}{solver_interface.ws_ptrs.iterations};\n')
    f.write(f'  {configuration.prefix}CPG_Info.status = {result_prefix}{solver_interface.ws_ptrs.status};\n')
    f.write(f'  {configuration.prefix}CPG_Info.pri_res = {result_prefix}{solver_interface.ws_ptrs.primal_residual};\n')
    f.write(f'  {configuration.prefix}CPG_Info.dua_res = {result_prefix}{solver_interface.ws_ptrs.dual_residual};\n')
    f.write('}\n\n')

    f.write('// Solve via canonicalization, canonical solve, retrieval\n')
    f.write(f'void {configuration.prefix}cpg_solve(){{\n')
    f.write('  // Canonicalize if necessary\n')

    for p_id, changes in parameter_canon.p_id_to_changes.items():
        if changes:
            f.write(f'  if ({configuration.prefix}Canon_Outdated.{p_id}) {{\n')
            f.write(f'    {configuration.prefix}cpg_canonicalize_{p_id}();\n')
            f.write('  }\n')
            if solver_interface.inmemory_preconditioning:
                size = parameter_canon.p_id_to_size[p_id]
                if size == 1:
                    f.write(f'  {configuration.prefix}Canon_Params_conditioning.{p_id} = {configuration.prefix}Canon_Params.{p_id};\n')
                elif size > 1:
                    f.write(f'  for (i=0; i<{size}; i++){{\n')
                    if p_id.isupper():
                        f.write(f'    {configuration.prefix}Canon_Params_conditioning.{p_id}->x[i] = {configuration.prefix}Canon_Params.{p_id}->x[i];\n')
                    else:
                        f.write(f'    {configuration.prefix}Canon_Params_conditioning.{p_id}[i] = {configuration.prefix}Canon_Params.{p_id}[i];\n')
                    f.write('  }\n')

    pus = solver_interface.parameter_update_structure
    write_update_structure(f, configuration, parameter_canon, pus, *analyze_pus(pus, parameter_canon.p_id_to_changes))

    if solver_interface.stgs_dynamically_allocated:
        for name in solver_interface.stgs_names_to_type.keys():
            f.write(f'  {configuration.prefix}{solver_interface.ws_ptrs.settings.format(setting_name=name)} = {configuration.prefix}Canon_Settings.{name};\n')

    f.write(f'  // Solve with {configuration.solver_name}\n')
    f.write(f'  {solver_interface.solve_function_call.format(prefix=configuration.prefix)};\n')

    f.write('  // Retrieve results\n')
    if solver_interface.ret_prim_func_exists(variable_info):
        f.write(f'  {configuration.prefix}cpg_retrieve_prim();\n')
    if solver_interface.ret_dual_func_exists(dual_variable_info):
        f.write(f'  {configuration.prefix}cpg_retrieve_dual();\n')
    f.write(f'  {configuration.prefix}cpg_retrieve_info();\n')

    f.write('  // Reset flags for outdated canonical parameters\n')
    for p_id, changes in parameter_canon.p_id_to_changes.items():
        if changes:
            f.write(f'  {configuration.prefix}Canon_Outdated.{p_id} = 0;\n')

    f.write('}\n\n')

    f.write('// Update solver settings\n')
    f.write(f'void {configuration.prefix}cpg_set_solver_default_settings(){{\n')
    if solver_interface.stgs_reset_function is not None:
        f.write(f'  {solver_interface.stgs_reset_function["name"]}({solver_interface.stgs_reset_function["ptr"] if solver_interface.stgs_reset_function["ptr"] is not None else "&" + configuration.prefix + "Canon_Settings"});\n')
    else:
        for name, value in solver_interface.stgs_names_to_default.items():
            f.write(f'  {configuration.prefix}Canon_Settings.{name} = {value};\n')
    f.write('}\n')
    for name, typ in solver_interface.stgs_names_to_type.items():
        f.write(f'\nvoid {configuration.prefix}cpg_set_solver_{name}({typ} {name}_new){{\n')
        if solver_interface.stgs_direct_write_ptr is not None:
            f.write(f'  {solver_interface.stgs_direct_write_ptr}->{name} = {name}_new;\n')
        else:
            f.write(f'  {configuration.prefix}Canon_Settings.{name} = {name}_new;\n')
        f.write('}\n')


def write_solve_prot(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface):
    """
    Write function declarations to file
    """

    write_description(f, 'c', 'Function declarations')
    f.write('#include "cpg_workspace.h"\n')

    f.write('\n// Update user-defined parameter values\n')
    for name, size in parameter_info.name_to_size_usp.items():
        if size == 1:
            f.write(f'extern void {configuration.prefix}cpg_update_{name}(cpg_float val);\n')
        else:
            f.write(f'extern void {configuration.prefix}cpg_update_{name}(cpg_int idx, cpg_float val);\n')

    f.write('\n// Map user-defined to canonical parameters\n')
    for p_id, changes in parameter_canon.p_id_to_changes.items():
        if changes:
            f.write(f'extern void {configuration.prefix}cpg_canonicalize_{p_id}();\n')

    if solver_interface.ret_prim_func_exists(variable_info):
        f.write('\n// Retrieve primal solution in terms of user-defined variables\n')
        f.write(f'extern void {configuration.prefix}cpg_retrieve_prim();\n')

    if solver_interface.ret_dual_func_exists(dual_variable_info):
        f.write('\n// Retrieve dual solution in terms of user-defined constraints\n')
        f.write(f'extern void {configuration.prefix}cpg_retrieve_dual();\n')

    f.write('\n// Retrieve solver information\n')
    f.write(f'extern void {configuration.prefix}cpg_retrieve_info();\n')

    f.write('\n// Solve via canonicalization, canonical solve, retrieval\n')
    f.write(f'extern void {configuration.prefix}cpg_solve();\n')

    f.write('\n// Update solver settings\n')
    f.write(f'extern void {configuration.prefix}cpg_set_solver_default_settings();\n')
    for name, typ in solver_interface.stgs_names_to_type.items():
        f.write(f'extern void {configuration.prefix}cpg_set_solver_{name}({typ} {name}_new);\n')
       
       

def write_gradient_def(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface):
    """
    Write parameter initialization function to file
    """
    
    n = solver_interface.n_var
    nl = solver_interface.n_eq
    nu = solver_interface.n_eq + solver_interface.n_ineq
    N = n + nu
    NP = len(parameter_info.flat_usp)

    write_description(f, 'c', 'Function definitions')
    f.write('#include "cpg_gradient.h"\n')
    f.write('#include "qdldl.h"\n')
    f.write('#include "qdldl_interface.h"\n\n')

    f.write('static cpg_int i;\n')
    f.write('static cpg_int j;\n')
    f.write('static cpg_int k;\n')
    
    f.write('\n// Update user-defined variable deltas\n')
    for name, size in variable_info.name_to_size.items():
        if size == 1:
            f.write(f'void {configuration.prefix}cpg_update_delta_{name}(cpg_float val){{\n')
            f.write(f'    {configuration.prefix}cpg_canon_delta_x[{variable_info.name_to_offset[name]}] = val;\n')
        else:
            f.write(f'void {configuration.prefix}cpg_update_delta_{name}(cpg_int idx, cpg_float val){{\n')
            f.write(f'    {configuration.prefix}cpg_canon_delta_x[{variable_info.name_to_offset[name]}+idx] = val;\n')
        f.write('}\n')

    f.write('\n// Compute gradient through canonical solver to vector parameters\n')
    f.write('void cpg_canon_gradient_vec(const OSQPSolver* solver, const cpg_float* dx, cpg_float* dqlu){\n')
    f.write('    qdldl_solver* qdldl = (qdldl_solver*) solver->work->linsys_solver;\n')
    f.write('    // Fill d and rhs\n')
    f.write(f'    for(i=0; i<{n}; i++){{\n')
    f.write(f'        {configuration.prefix}cpg_canon_gradient_d[i] = dx[i];\n')
    f.write(f'        {configuration.prefix}cpg_canon_gradient_rhs[i] = dx[i];\n')
    f.write('    }\n')
    f.write(f'    for(i={n}; i<{N}; i++){{\n')
    f.write(f'        {configuration.prefix}cpg_canon_gradient_d[i] = 0.0;\n')
    f.write(f'        {configuration.prefix}cpg_canon_gradient_rhs[i] = 0.0;\n')
    f.write('    }\n')
    f.write(f'    QDLDL_solve({N}, qdldl->L->p, qdldl->L->i, qdldl->L->x, qdldl->Dinv, {configuration.prefix}cpg_canon_gradient_d);\n')
    f.write('    // Refine iteratively\n')
    f.write(f'    for(i=0; i<{configuration.gradient_refine_iters}; i++){{\n')
    f.write('        // Compute r\n')
    f.write(f'        for(j=0; j<{n}; j++){{\n')
    f.write(f'            {configuration.prefix}cpg_canon_gradient_r[j] = {configuration.prefix}cpg_canon_gradient_rhs[j];\n')
    f.write(f'            for(k={configuration.prefix}canon_P.p[j]; k<{configuration.prefix}canon_P.p[j+1]; k++){{\n')
    f.write(f'                {configuration.prefix}cpg_canon_gradient_r[j] -= {configuration.prefix}canon_P.x[k]*{configuration.prefix}cpg_canon_gradient_d[{configuration.prefix}canon_P.i[k]];\n')
    f.write('            }\n')
    f.write(f'            for(k={configuration.prefix}canon_A.p[j]; k<{configuration.prefix}canon_A.p[j+1]; k++){{\n')
    f.write(f'                {configuration.prefix}cpg_canon_gradient_r[j] -= {configuration.prefix}canon_A.x[k]*{configuration.prefix}cpg_canon_gradient_d[{configuration.prefix}canon_A.i[k]+{n}];\n')
    f.write('            }\n')
    f.write('        }\n')
    f.write(f'        for(j={n}; j<{N}; j++){{\n')
    f.write(f'            {configuration.prefix}cpg_canon_gradient_r[j] = {configuration.prefix}cpg_canon_gradient_rhs[j];\n')
    f.write('        }\n')
    f.write(f'        for(j=0; j<{n}; j++){{\n')
    f.write(f'            for(k={configuration.prefix}canon_A.p[j]; k<{configuration.prefix}canon_A.p[j+1]; k++){{\n')
    f.write(f'                {configuration.prefix}cpg_canon_gradient_r[{configuration.prefix}canon_A.i[k]+{n}] -= {configuration.prefix}canon_A.x[k]*{configuration.prefix}cpg_canon_gradient_d[j];\n')
    f.write('            }\n')
    f.write('        }\n')
    f.write('        // Solve residual system\n')
    f.write(f'        QDLDL_solve({N}, qdldl->L->p, qdldl->L->i, qdldl->L->x, qdldl->Dinv, {configuration.prefix}cpg_canon_gradient_r);\n')
    f.write('        // Update d\n')
    f.write(f'        for(j=0; j<{N}; j++){{\n')
    f.write(f'            {configuration.prefix}cpg_canon_gradient_d[j] += {configuration.prefix}cpg_canon_gradient_r[j];\n')
    f.write('        }\n')
    f.write('    }\n')
    f.write('    // Fill dqlu\n')
    f.write(f'    for(i=0; i<{n}; i++){{\n')
    f.write(f'        dqlu[i] = {configuration.prefix}cpg_canon_gradient_d[i];\n')
    f.write('    }\n')
    f.write(f'    for(i={n}; i<{N}; i++){{\n')
    f.write(f'        if (solver->solution->y[i-{n}] < -1e-12) {{\n')
    f.write(f'            dqlu[i] = {configuration.prefix}cpg_canon_gradient_d[i];\n')
    f.write(f'            dqlu[{nl}+i] = 0.0;\n')
    f.write(f'        }} else if (solver->solution->y[i-{n}] > 1e-12) {{\n')
    f.write(f'            dqlu[i] = 0.0;\n')
    f.write(f'            dqlu[{nl}+i] = {configuration.prefix}cpg_canon_gradient_d[i];\n')
    f.write('        } else {\n')
    f.write(f'            dqlu[i] = 0.0;\n')
    f.write(f'            dqlu[{nl}+i] = 0.0;\n')
    f.write('        }\n')
    f.write('    }\n')
    f.write('}\n\n')
    
    # todo: change other bound to conserve feasibility
    
    f.write('\n// Compute gradient through canonical solver to matrix parameters\n')
    f.write('// cpg_canon_gradient_vec to be called first\n')
    f.write('void cpg_canon_gradient_mat(const OSQPSolver* solver, const cpg_float* d, const cpg_float* x, cpg_float* dP, cpg_float* dA){\n')
    if parameter_canon.p_id_to_changes['P']:
        f.write(f'    for(j=0; j<{n}; j++){{\n')
        f.write('        for(k=canon_P.p[j]; k<canon_P.p[j+1]; k++){\n')
        f.write('            dP[k] = -0.5 * (d[canon_P.i[k]] * x[j] + x[canon_P.i[k]] * d[j]);\n')
        f.write('        }\n')
        f.write('    }\n')
    if parameter_canon.p_id_to_changes['A']:
        f.write(f'    for(j=0; j<{n}; j++){{\n')
        f.write('        for(k=canon_A.p[j]; k<canon_A.p[j+1]; k++){\n')
        f.write('            if (fabs(solver->solution->y[canon_A.i[k]]) > 1e-12) {\n')
        f.write(f'                dA[k] = - solver->solution->y[canon_A.i[k]] * d[j] - d[{n} + canon_A.i[k]] * x[j];\n')
        f.write('            } else {\n')
        f.write(f'                dA[k] = 0.0;\n')
        f.write('            }\n')
        f.write('        }\n')
        f.write('    }\n')
    f.write('}\n\n')
    
    f.write('\n// Un-canonicalize\n')
    f.write(f'void {configuration.prefix}cpg_uncanonicalize_vec(const cpg_float* dqlu, cpg_float* delta_p){{\n')
    f.write(f'    for(j=0; j<{NP}; j++){{\n')
    f.write(f'        delta_p[j] = 0.0;\n')
    f.write('    }\n')
    if parameter_canon.p_id_to_changes['q']:
        f.write(f'    for(j=0; j<{n}; j++){{\n')
        f.write(f'        for(k={configuration.prefix}canon_q_map.p[j]; k<{configuration.prefix}canon_q_map.p[j+1]; k++){{\n')
        f.write(f'            delta_p[{configuration.prefix}canon_q_map.i[k]] += {configuration.prefix}canon_q_map.x[k]*{configuration.prefix}dqlu[j];\n')
        f.write('        }\n')
        f.write('    }\n')
    if parameter_canon.p_id_to_changes['l']:
        f.write(f'    for(j=0; j<{nl}; j++){{\n')
        f.write(f'        for(k={configuration.prefix}canon_l_map.p[j]; k<{configuration.prefix}canon_l_map.p[j+1]; k++){{\n')
        f.write(f'            delta_p[{configuration.prefix}canon_l_map.i[k]] += {configuration.prefix}canon_l_map.x[k]*{configuration.prefix}dqlu[j+{n}];\n')
        f.write('        }\n')
        f.write('    }\n')
    if parameter_canon.p_id_to_changes['u']:
        f.write(f'    for(j=0; j<{nu}; j++){{\n')
        f.write(f'        for(k={configuration.prefix}canon_u_map.p[j]; k<{configuration.prefix}canon_u_map.p[j+1]; k++){{\n')
        f.write(f'            delta_p[{configuration.prefix}canon_u_map.i[k]] += {configuration.prefix}canon_u_map.x[k]*{configuration.prefix}dqlu[j+{n+nl}];\n')
        f.write('        }\n')
        f.write('    }\n')
    f.write('}\n\n')
    
    f.write('// cpg_uncanonicalize_vec to be called first\n')
    f.write(f'void {configuration.prefix}cpg_uncanonicalize_mat(const cpg_float* dP, const cpg_float* dA, cpg_float* delta_p){{\n')
    if parameter_canon.p_id_to_changes['P']:
        f.write(f'    for(j=0; j<{parameter_canon.p_csc["P"].nnz}; j++){{\n')
        f.write(f'        for(k={configuration.prefix}canon_P_map.p[j]; k<{configuration.prefix}canon_P_map.p[j+1]; k++){{\n')
        f.write(f'            delta_p[{configuration.prefix}canon_P_map.i[k]] += {configuration.prefix}canon_P_map.x[k]*{configuration.prefix}dP[j];\n')
        f.write('        }\n')
        f.write('    }\n')
    if parameter_canon.p_id_to_changes['A']:
        f.write(f'    for(j=0; j<{parameter_canon.p_csc["A"].nnz}; j++){{\n')
        f.write(f'        for(k={configuration.prefix}canon_A_map.p[j]; k<{configuration.prefix}canon_A_map.p[j+1]; k++){{\n')
        f.write(f'            delta_p[{configuration.prefix}canon_A_map.i[k]] += {configuration.prefix}canon_A_map.x[k]*{configuration.prefix}dA[j];\n')
        f.write('        }\n')
        f.write('    }\n')
    f.write('}\n\n')
    
    f.write('\n// End-to-end gradient\n')
    f.write(f'void {configuration.prefix}cpg_gradient(){{\n')
    f.write(f'    {configuration.prefix}cpg_canon_gradient_vec(&solver, {configuration.prefix}cpg_canon_delta_x, {configuration.prefix}cpg_canon_gradient_dqlu);\n')
    f.write(f'    {configuration.prefix}cpg_canon_gradient_mat(&solver, {configuration.prefix}cpg_canon_gradient_d, sol_x, '
            f'{configuration.prefix}cpg_canon_gradient_dP, {configuration.prefix}cpg_canon_gradient_dA);\n')
    f.write(f'    {configuration.prefix}cpg_uncanonicalize_vec({configuration.prefix}cpg_canon_gradient_dqlu, {configuration.prefix}cpg_delta_p);\n')
    f.write(f'    {configuration.prefix}cpg_uncanonicalize_mat({configuration.prefix}cpg_canon_gradient_dP, {configuration.prefix}cpg_canon_gradient_dA, '
            f'{configuration.prefix}cpg_delta_p);\n')
    f.write(f'    for(i=0; i<{n}; i++){{\n')
    f.write(f'        {configuration.prefix}cpg_canon_delta_x[i] = 0.0;\n')
    f.write('    }\n')
    f.write('}\n\n')
        
        
def write_gradient_prot(f, configuration, variable_info, dual_variable_info, parameter_info, parameter_canon, solver_interface):
    """
    Write function declarations to file
    """

    write_description(f, 'c', 'Function declarations')
    f.write('#include "cpg_workspace.h"\n')
    f.write('#include "osqp_api_types.h"\n')
    
    f.write('\n// Un-retrieve\n')
    for name, size in variable_info.name_to_size.items():
        if size == 1:
            f.write(f'extern void {configuration.prefix}cpg_update_delta_{name}(cpg_float val);\n')
        else:
            f.write(f'extern void {configuration.prefix}cpg_update_delta_{name}(cpg_int idx, cpg_float val);\n')
    
    f.write('\n// Gradient through canonical solver\n')
    f.write(f'extern void {configuration.prefix}cpg_canon_gradient_vec(const OSQPSolver* solver, const cpg_float* dx, cpg_float* dqlu);\n')
    f.write(f'extern void {configuration.prefix}cpg_canon_gradient_mat(const OSQPSolver* solver, const cpg_float* d, const cpg_float* x, cpg_float* dP, cpg_float* dA);\n')
    
    f.write('\n// Un-canonicalize\n')
    f.write(f'extern void {configuration.prefix}cpg_uncanonicalize_vec(const cpg_float* dqlu, cpg_float* delta_p);\n')
    f.write(f'extern void {configuration.prefix}cpg_uncanonicalize_mat(const cpg_float* dP, const cpg_float* dA, cpg_float* delta_p);\n')
    
    f.write('\n// End-to-end gradient\n')
    f.write(f'extern void {configuration.prefix}cpg_gradient();\n')


def write_example_def(f, configuration, variable_info, dual_variable_info, parameter_info):
    """
    Write main function to file
    """

    write_description(f, 'c', 'Example program for updating parameters, solving, and inspecting the result')
    f.write('#include <stdio.h>\n')
    f.write('#include "cpg_workspace.h"\n')
    f.write('#include "cpg_solve.h"\n\n')
    f.write('static int i;\n\n')

    f.write('int main(int argc, char *argv[]){\n\n')

    f.write('  // Update first entry of every user-defined parameter\n')
    for name, value in parameter_info.writable.items():
        if is_mathematical_scalar(value):
            f.write(f'  {configuration.prefix}cpg_update_{name}(%.20f);\n' % value)
        else:
            f.write(f'  {configuration.prefix}cpg_update_{name}(0, %.20f);\n' % value[0])

    f.write('\n  // Solve the problem instance\n')
    f.write(f'  {configuration.prefix}cpg_solve();\n\n')

    f.write('  // Print objective function value\n')
    f.write(f'  printf("obj = %f\\n", {configuration.prefix}CPG_Result.info->obj_val);\n\n')

    f.write('  // Print primal solution\n')

    for name, var in variable_info.name_to_init.items():
        if is_mathematical_scalar(var):
            f.write(f'  printf("{name} = %f\\n", {configuration.prefix}CPG_Result.prim->{name});\n')
        else:
            f.write(f'  for(i=0; i<{var.size}; i++) {{\n')
            f.write(f'    printf("{name}[%d] = %f\\n", i, {configuration.prefix}CPG_Result.prim->{name}[i]);\n')
            f.write(f'  }}\n')

    if len(dual_variable_info.name_to_init) > 0:
        f.write('\n  // Print dual solution\n')
    for name, var in dual_variable_info.name_to_init.items():
        if is_mathematical_scalar(var):
            f.write(f'  printf("{name} = %f\\n", {configuration.prefix}CPG_Result.dual->{name});\n')
        else:
            f.write(f'  for(i=0; i<{var.size}; i++) {{\n')
            f.write(f'    printf("{name}[%d] = %f\\n", i, {configuration.prefix}CPG_Result.dual->{name}[i]);\n')
            f.write('  }\n')

    f.write('\n  return 0;\n\n')
    f.write('}\n')


def replace_cmake_data(cmake_data, configuration):
    """
    Add configuration.prefix to directory/file lists in top-level CMakeLists.txt
    """

    now = datetime.now()
    cmake_data = cmake_data.replace('%DATE', now.strftime("on %B %d, %Y at %H:%M:%S"))
    cmake_data = cmake_data.replace('cpg_include', configuration.prefix+'cpg_include')
    cmake_data = cmake_data.replace('cpg_head', configuration.prefix + 'cpg_head')
    if configuration.gradient:
        cmake_data = cmake_data.replace(
            '${solver_head}',
            '${CMAKE_CURRENT_SOURCE_DIR}/include/cpg_gradient.h\n      ${solver_head}')
        cmake_data = cmake_data.replace(
            '${solver_src}',
            '${CMAKE_CURRENT_SOURCE_DIR}/src/cpg_gradient.c\n      ${solver_src}')
    return cmake_data.replace('cpg_src', configuration.prefix + 'cpg_src')


def write_canon_cmake(f, configuration, solver_interface):
    """
    Pass sources to parent scope in <solver_code>/CMakeLists.txt
    """

    if len(solver_interface.cmake_headers) > 0:
        f.write('\nfile(\n  GLOB\n  SOLVER_HEAD')
        for h in solver_interface.cmake_headers:
            f.write('\n  ' + h)
        f.write(')\n')
        f.write('\nset(solver_head ${SOLVER_HEAD} PARENT_SCOPE)\n')
    if len(solver_interface.cmake_sources) > 0:
        f.write('\nfile(\n  GLOB\n  SOLVER_SRC')
        for h in solver_interface.cmake_sources:
            f.write('\n  ' + h)
        f.write(')')
        f.write('\nset(solver_src ${SOLVER_SRC} PARENT_SCOPE)\n')


def write_module_def(f, configuration, variable_info, dual_variable_info, parameter_info, solver_interface):
    """
    Write c++ file for pbind11 wrapper
    """

    write_description(f, 'cpp', 'Python binding with pybind11')
    f.write('#include <pybind11/pybind11.h>\n')
    f.write('#include <pybind11/stl.h>\n')
    f.write('#include <ctime>\n')
    f.write('#include "cpg_module.hpp"\n\n')
    f.write('extern "C" {\n')
    f.write('    #include "include/cpg_workspace.h"\n')
    f.write('    #include "include/cpg_solve.h"\n')
    if configuration.gradient:
        f.write('    #include "include/cpg_gradient.h"\n')
    f.write('}\n\n')
    f.write('namespace py = pybind11;\n\n')
    if max(
            max(parameter_info.name_to_size_usp.values(), default=0),
            max(variable_info.name_to_size.values(), default=0),
            max(dual_variable_info.name_to_size.values(), default=0)
    ) > 1:
        f.write('static int i;\n\n')

    # cpp function that maps parameters to results
    f.write(f'{configuration.prefix}CPG_Result_cpp_t {configuration.prefix}solve_cpp(struct {configuration.prefix}CPG_Updated_cpp_t& CPG_Updated_cpp, '
            f'struct {configuration.prefix}CPG_Params_cpp_t& CPG_Params_cpp){{\n\n')

    f.write('    // Pass changed user-defined parameter values to the solver\n')
    for name, size in parameter_info.name_to_size_usp.items():
        f.write(f'    if (CPG_Updated_cpp.{name}) {{\n')
        if size == 1:
            f.write(f'        {configuration.prefix}cpg_update_{name}(CPG_Params_cpp.{name});\n')
        else:
            f.write(f'        for(i=0; i<{size}; i++) {{\n')
            f.write(f'            {configuration.prefix}cpg_update_{name}(i, CPG_Params_cpp.{name}[i]);\n')
            f.write(f'        }}\n')
        f.write('    }\n')

    # perform ASA procedure
    f.write('\n    // Solve\n')
    f.write('    std::clock_t ASA_start = std::clock();\n')
    f.write(f'    {configuration.prefix}cpg_solve();\n')
    f.write('    std::clock_t ASA_end = std::clock();\n\n')

    # arrange and return results
    f.write('    // Arrange and return results\n')

    f.write(f'    {configuration.prefix}CPG_Prim_cpp_t CPG_Prim_cpp {{}};\n')
    for name, var in variable_info.name_to_init.items():
        if is_mathematical_scalar(var):
            f.write(f'    CPG_Prim_cpp.{name} = {configuration.prefix}CPG_Prim.{name};\n')
        else:
            f.write(f'    for(i=0; i<{var.size}; i++) {{\n')
            f.write(f'        CPG_Prim_cpp.{name}[i] = {configuration.prefix}CPG_Prim.{name}[i];\n')
            f.write('    }\n')

    if len(dual_variable_info.name_to_init) > 0:
        f.write(f'    {configuration.prefix}CPG_Dual_cpp_t CPG_Dual_cpp {{}};\n')
        for name, var in dual_variable_info.name_to_init.items():
            if is_mathematical_scalar(var):
                f.write(f'    CPG_Dual_cpp.{name} = {configuration.prefix}CPG_Dual.{name};\n')
            else:
                f.write(f'    for(i=0; i<{var.size}; i++) {{\n')
                f.write(f'        CPG_Dual_cpp.{name}[i] = {configuration.prefix}CPG_Dual.{name}[i];\n')
                f.write('    }\n')

    f.write(f'    {configuration.prefix}CPG_Info_cpp_t CPG_Info_cpp {{}};\n')
    for field in ['obj_val', 'iter', 'status', 'pri_res', 'dua_res']:
        f.write(f'    CPG_Info_cpp.{field} = {configuration.prefix}CPG_Info.{field};\n')
    f.write('    CPG_Info_cpp.time = 1.0 * (ASA_end - ASA_start) / CLOCKS_PER_SEC;\n')

    f.write(f'    {configuration.prefix}CPG_Result_cpp_t CPG_Result_cpp {{}};\n')
    f.write('    CPG_Result_cpp.prim = CPG_Prim_cpp;\n')
    if len(dual_variable_info.name_to_init) > 0:
        f.write('    CPG_Result_cpp.dual = CPG_Dual_cpp;\n')
    f.write('    CPG_Result_cpp.info = CPG_Info_cpp;\n')

    # return
    f.write('    return CPG_Result_cpp;\n\n')
    f.write('}\n\n')
    
    if configuration.gradient:
        # cpp function that maps variable deltas to parameter deltas
        f.write(f'{configuration.prefix}CPG_PDelta_cpp_t {configuration.prefix}gradient_cpp(struct {configuration.prefix}CPG_VDelta_cpp_t& CPG_VDelta_cpp){{\n\n')

        f.write('    // Pass user-defined variable deltas to the solver\n')
        for name, var in variable_info.name_to_init.items():
            if size == 1:
                f.write(f'    {configuration.prefix}cpg_update_delta_{name}(CPG_VDelta_cpp.{name});\n')
            else:
                f.write(f'    for(i=0; i<{size}; i++) {{\n')
                f.write(f'        {configuration.prefix}cpg_update_delta_{name}(i, CPG_VDelta_cpp.{name}[i]);\n')
                f.write(f'    }}\n')

        # perform ASA gradient
        f.write('\n    // Compute gradient\n')
        f.write(f'    {configuration.prefix}cpg_gradient();\n\n')

        # arrange and return results
        f.write('    // Arrange and return results\n')

        f.write(f'    {configuration.prefix}CPG_PDelta_cpp_t CPG_PDelta_cpp {{}};\n')
        for name, size in parameter_info.name_to_size_usp.items():
            if size == 1:
                f.write(f'    CPG_PDelta_cpp.{name} = {configuration.prefix}CPG_Delta.{name};\n')
            else:
                f.write(f'    for(i=0; i<{var.size}; i++) {{\n')
                f.write(f'        CPG_PDelta_cpp.{name}[i] = {configuration.prefix}CPG_Delta.{name}[i];\n')
                f.write('    }\n')

        # return
        f.write('    return CPG_PDelta_cpp;\n\n')
        f.write('}\n\n')

    # module
    f.write('PYBIND11_MODULE(cpg_module, m) {\n\n')

    f.write(f'    py::class_<{configuration.prefix}CPG_Params_cpp_t>(m, "{configuration.prefix}cpg_params")\n')
    f.write('            .def(py::init<>())\n')
    for name in parameter_info.name_to_size_usp.keys():
        f.write(f'            .def_readwrite("{name}", &{configuration.prefix}CPG_Params_cpp_t::{name})\n')
    f.write('            ;\n\n')

    f.write(f'    py::class_<{configuration.prefix}CPG_Updated_cpp_t>(m, "{configuration.prefix}cpg_updated")\n')
    f.write('            .def(py::init<>())\n')
    for name in parameter_info.name_to_size_usp.keys():
        f.write(f'            .def_readwrite("{name}", &{configuration.prefix}CPG_Updated_cpp_t::{name})\n')
    f.write('            ;\n\n')

    f.write(f'    py::class_<{configuration.prefix}CPG_Prim_cpp_t>(m, "{configuration.prefix}cpg_prim")\n')
    f.write('            .def(py::init<>())\n')
    for name in variable_info.name_to_init.keys():
        f.write(f'            .def_readwrite("{name}", &{configuration.prefix}CPG_Prim_cpp_t::{name})\n')
    f.write('            ;\n\n')

    if len(dual_variable_info.name_to_init) > 0:
        f.write(f'    py::class_<{configuration.prefix}CPG_Dual_cpp_t>(m, "{configuration.prefix}cpg_dual")\n')
        f.write('            .def(py::init<>())\n')
        for name in dual_variable_info.name_to_init.keys():
            f.write(f'            .def_readwrite("{name}", &{configuration.prefix}CPG_Dual_cpp_t::{name})\n')
        f.write('            ;\n\n')

    f.write(f'    py::class_<{configuration.prefix}CPG_Info_cpp_t>(m, "{configuration.prefix}cpg_info")\n')
    f.write('            .def(py::init<>())\n')
    f.write(f'            .def_readwrite("obj_val", &{configuration.prefix}CPG_Info_cpp_t::obj_val)\n')
    f.write(f'            .def_readwrite("iter", &{configuration.prefix}CPG_Info_cpp_t::iter)\n')
    f.write(f'            .def_readwrite("status", &{configuration.prefix}CPG_Info_cpp_t::status)\n')
    f.write(f'            .def_readwrite("pri_res", &{configuration.prefix}CPG_Info_cpp_t::pri_res)\n')
    f.write(f'            .def_readwrite("dua_res", &{configuration.prefix}CPG_Info_cpp_t::dua_res)\n')
    f.write(f'            .def_readwrite("time", &{configuration.prefix}CPG_Info_cpp_t::time)\n')
    f.write('            ;\n\n')

    f.write(f'    py::class_<{configuration.prefix}CPG_Result_cpp_t>(m, "{configuration.prefix}cpg_result")\n')
    f.write('            .def(py::init<>())\n')
    f.write(f'            .def_readwrite("cpg_prim", &{configuration.prefix}CPG_Result_cpp_t::prim)\n')
    if len(dual_variable_info.name_to_init) > 0:
        f.write(f'            .def_readwrite("cpg_dual", &{configuration.prefix}CPG_Result_cpp_t::dual)\n')
    f.write(f'            .def_readwrite("cpg_info", &{configuration.prefix}CPG_Result_cpp_t::info)\n')
    f.write('            ;\n\n')

    f.write(f'    m.def("solve", &{configuration.prefix}solve_cpp);\n\n')

    if configuration.gradient:
        f.write(f'    py::class_<{configuration.prefix}CPG_VDelta_cpp_t>(m, "{configuration.prefix}cpg_vdelta")\n')
        f.write('            .def(py::init<>())\n')
        for name in variable_info.name_to_init.keys():
            f.write(f'            .def_readwrite("{name}", &{configuration.prefix}CPG_VDelta_cpp_t::{name})\n')
        f.write('            ;\n\n')
        
        f.write(f'    py::class_<{configuration.prefix}CPG_PDelta_cpp_t>(m, "{configuration.prefix}cpg_pdelta")\n')
        f.write('            .def(py::init<>())\n')
        for name in parameter_info.name_to_size_usp.keys():
            f.write(f'            .def_readwrite("{name}", &{configuration.prefix}CPG_PDelta_cpp_t::{name})\n')
        f.write('            ;\n\n')
        f.write(f'    m.def("gradient", &{configuration.prefix}gradient_cpp);\n\n')

    f.write(f'    m.def("set_solver_default_settings", &{configuration.prefix}cpg_set_solver_default_settings);\n')
    for name in solver_interface.stgs_names_to_type.keys():
        f.write(f'    m.def("set_solver_{name}", &{configuration.prefix}cpg_set_solver_{name});\n')

    f.write('\n}\n')


def write_module_prot(f, configuration, parameter_info, variable_info, dual_variable_info, solver_interface):
    """
    Write c++ file for pybind11 wrapper
    """

    write_description(f, 'cpp', 'Declarations for Python binding with pybind11')

    # cpp struct containing user-defined parameters
    f.write('// User-defined parameters\n')
    f.write(f'struct {configuration.prefix}CPG_Params_cpp_t {{\n')
    for name, size in parameter_info.name_to_size_usp.items():
        array_decl = f'std::array<double, {size}>' if size > 1 else 'double'
        f.write(f'    {array_decl} {name};\n')
    f.write('};\n\n')

    # cpp struct containing update flags for user-defined parameters
    f.write('// Flags for updated user-defined parameters\n')
    f.write(f'struct {configuration.prefix}CPG_Updated_cpp_t {{\n')
    for name in parameter_info.name_to_size_usp.keys():
        f.write(f'    bool {name};\n')
    f.write('};\n\n')

    # cpp struct containing primal variables
    f.write(f'// Primal solution\n')
    f.write(f'struct {configuration.prefix}CPG_Prim_cpp_t {{\n')
    for name, var in variable_info.name_to_init.items():
        if is_mathematical_scalar(var):
            f.write(f'    double {name};\n')
        else:
            f.write(f'    std::array<double, {var.size}> {name};\n')
    f.write('};\n\n')

    # cpp struct containing dual variables
    if len(dual_variable_info.name_to_init) > 0:
        f.write('// Dual solution\n')
        f.write(f'struct {configuration.prefix}CPG_Dual_cpp_t {{\n')
        for name, var in dual_variable_info.name_to_init.items():
            if is_mathematical_scalar(var):
                f.write(f'    double {name};\n')
            else:
                f.write(f'    std::array<double, {var.size}> {name};\n')
        f.write('};\n\n')

    # cpp struct containing info on results
    f.write('// Solver information\n')
    f.write(f'struct {configuration.prefix}CPG_Info_cpp_t {{\n')
    f.write('    double obj_val;\n')
    f.write('    int iter;\n')
    f.write(f'    { "int" if solver_interface.status_is_int else "char*"} status;\n')
    f.write('    double pri_res;\n')
    f.write('    double dua_res;\n')
    f.write('    double time;\n')
    f.write('};\n\n')

    # cpp struct containing objective value and user-defined variables
    f.write('// Solution and solver information\n')
    f.write(f'struct {configuration.prefix}CPG_Result_cpp_t {{\n')
    f.write(f'    {configuration.prefix}CPG_Prim_cpp_t prim;\n')
    if len(dual_variable_info.name_to_init) > 0:
        f.write(f'    {configuration.prefix}CPG_Dual_cpp_t dual;\n')
    f.write(f'    {configuration.prefix}CPG_Info_cpp_t info;\n')
    f.write('};\n\n')

    # cpp function that maps parameters to results
    f.write(f'// Main solve function\n')
    f.write(f'{configuration.prefix}CPG_Result_cpp_t {configuration.prefix}solve_cpp(struct {configuration.prefix}CPG_Updated_cpp_t& CPG_Updated_cpp, '
            f'struct {configuration.prefix}CPG_Params_cpp_t& CPG_Params_cpp);\n')

    if configuration.gradient:
        # cpp struct containing primal variable deltas
        f.write(f'\n// Primal variable deltas\n')
        f.write(f'struct {configuration.prefix}CPG_VDelta_cpp_t {{\n')
        for name, var in variable_info.name_to_init.items():
            if is_mathematical_scalar(var):
                f.write(f'    double {name};\n')
            else:
                f.write(f'    std::array<double, {var.size}> {name};\n')
        f.write('};\n\n')
        
        # cpp struct containing parameter deltas
        if configuration.gradient:
            f.write(f'// Parameter deltas\n')
            f.write(f'struct {configuration.prefix}CPG_PDelta_cpp_t {{\n')
            for name, size in parameter_info.name_to_size_usp.items():
                if size == 1:
                    f.write(f'    double {name};\n')
                else:
                    f.write(f'    std::array<double, {size}> {name};\n')
            f.write('};\n\n')
            
        # cpp function that maps variable deltas to parameter deltas
        f.write(f'// Derivative function\n')
        f.write(f'{configuration.prefix}CPG_PDelta_cpp_t {configuration.prefix}gradient_cpp(struct {configuration.prefix}CPG_VDelta_cpp_t& CPG_VDelta_cpp);\n')


def replace_setup_data(text):
    """
    Replace placeholder strings in setup.py file
    """

    # description
    now = datetime.now()
    return text.replace('%DATE', now.strftime("on %B %d, %Y at %H:%M:%S"))


def write_method(f, configuration, variable_info, dual_variable_info, parameter_info, solver_interface):
    """
    Write function to be registered as custom CVXPY solve method
    """

    write_description(f, 'py', 'Custom solve method for CVXPY interface')
    f.write('import time\n')
    f.write('import warnings\n')
    f.write('import numpy as np\n')
    f.write('from cvxpy.reductions import Solution\n')
    f.write('from cvxpy.problems.problem import SolverStats\n')
    f.write('from %s import cpg_module\n\n\n' % configuration.code_dir.replace('/', '.').replace('\\', '.'))

    f.write(f'standard_settings_names = {solver_interface.stgs_translation}\n\n\n')

    f.write('def cpg_solve(prob, updated_params=None, **kwargs):\n\n')
    f.write('    # set flags for updated parameters\n')
    f.write(f'    upd = cpg_module.{configuration.prefix}cpg_updated()\n')
    f.write('    if updated_params is None:\n')
    p_list_string = ', '.join([f'"{name}"' for name in parameter_info.name_to_size_usp.keys()])
    f.write(f'        updated_params = [{p_list_string}]\n')
    f.write('    for p in updated_params:\n')
    f.write('        try:\n')
    f.write('            setattr(upd, p, True)\n')
    f.write('        except AttributeError:\n')
    f.write('            raise AttributeError(f"{p} is not a parameter.")\n\n')

    f.write('    # set solver settings\n')
    f.write('    cpg_module.set_solver_default_settings()\n')
    f.write('    for key, value in kwargs.items():\n')
    f.write('        try:\n')
    f.write('            eval(f\'cpg_module.set_solver_{standard_settings_names.get(key, key)}(value)\')\n')
    f.write('        except AttributeError:\n')
    f.write('            raise AttributeError(f\'Solver setting "{key}" not available.\')\n\n')

    f.write('    # set parameter values\n')
    f.write(f'    par = cpg_module.{configuration.prefix}cpg_params()\n')
    f.write('    param_dict = prob.param_dict\n')
    for name, size in parameter_info.name_to_size_usp.items():
        if name in parameter_info.name_to_sparsity.keys():
            f.write(f'    n = param_dict["{name}"].shape[0]\n')
            if parameter_info.name_to_sparsity_type[name] == 'diag':
                f.write(f'    {name}_coordinates = np.arange(0, n**2, n+1)\n')
            else:
                f.write(f'    {name}_coordinates = np.unique([coord[0]+coord[1]*n for coord in '
                        f'param_dict["{name}"].attributes["sparsity"]])\n')
            if size == 1:
                f.write(f'    par.{name} = param_dict["{name}"].value[{name}_coordinates]\n')
            else:
                f.write(f'    {name}_value = []\n')
                f.write(f'    {name}_flat = param_dict["{name}"].value.flatten(order="F")\n')
                f.write(f'    for coord in {name}_coordinates:\n')
                f.write(f'        {name}_value.append({name}_flat[coord])\n')
                f.write(f'        {name}_flat[coord] = 0\n')
                f.write(f'    if np.sum(np.abs({name}_flat)) > 0:\n')
                f.write(f'        warnings.warn(\'Ignoring nonzero value outside of sparsity pattern for '
                        f'parameter {name}!\')\n')
                f.write(f'    par.{name} = list({name}_value)\n')
        else:
            if size == 1:
                f.write(f'    par.{name} = param_dict["{name}"].value\n')
            else:
                f.write(f'    par.{name} = list(param_dict["{name}"].value.flatten(order="F"))\n')

    f.write('\n    # solve\n')
    f.write('    t0 = time.time()\n')
    f.write('    res = cpg_module.solve(upd, par)\n')
    f.write('    t1 = time.time()\n\n')

    f.write('    # store solution in problem object\n')
    f.write('    prob._clear_solution()\n')
    for name, shape in variable_info.name_to_shape.items():
        if len(shape) == 2:
            f.write(f'    prob.var_dict[\'{name}\'].save_value(np.array(res.cpg_prim.{name}).reshape(({shape[0]}, {shape[1]}), order=\'F\'))\n')
        elif len(shape) == 1:
            f.write(f'    prob.var_dict[\'{name}\'].save_value(np.array(res.cpg_prim.{name}).reshape({shape[0]}))\n')
        else:
            f.write(f'    prob.var_dict[\'{name}\'].save_value(np.array(res.cpg_prim.{name}))\n')

    for i, (name, shape) in enumerate(dual_variable_info.name_to_shape.items()):
        if len(shape) == 2:
            f.write(f'    prob.constraints[{i}].save_dual_value(np.array(res.cpg_dual.{name}).reshape(({shape[0]}, {shape[1]}), order=\'F\'))\n')
        elif len(shape) == 1:
            f.write(f'    prob.constraints[{i}].save_dual_value(np.array(res.cpg_dual.{name}).reshape({shape[0]}))\n')
        else:
            f.write(f'    prob.constraints[{i}].save_dual_value(np.array(res.cpg_dual.{name}))\n')

    f.write('\n    # store additional solver information in problem object\n')
    f.write('    prob._status = %s\n' %
            (f'"%d (for description visit {solver_interface.docu})" % res.cpg_info.status' if solver_interface.status_is_int else 'res.cpg_info.status'))
    f.write('    if abs(res.cpg_info.obj_val) == 1e30:\n')
    f.write('        prob._value = np.sign(res.cpg_info.obj_val) * np.inf\n')
    f.write('    else:\n')
    f.write('        prob._value = res.cpg_info.obj_val\n')
    f.write('    primal_vars = {var.id: var.value for var in prob.variables()}\n')
    f.write('    dual_vars = {c.id: c.dual_value for c in prob.constraints}\n')
    f.write('    solver_specific_stats = {\'obj_val\': res.cpg_info.obj_val,\n')
    f.write('                             \'status\': prob._status,\n')
    f.write('                             \'iter\': res.cpg_info.iter,\n')
    f.write('                             \'pri_res\': res.cpg_info.pri_res,\n')
    f.write('                             \'dua_res\': res.cpg_info.dua_res,\n')
    f.write('                             \'time\': res.cpg_info.time}\n')
    f.write('    attr = {\'solve_time\': t1 - t0, \'solver_specific_stats\': solver_specific_stats, '
            '\'num_iters\': res.cpg_info.iter}\n')
    f.write('    prob._solution = Solution(prob.status, prob.value, primal_vars, dual_vars, attr)\n')
    f.write('    results_dict = {\'solver_specific_stats\': solver_specific_stats,\n')
    f.write('                    \'num_iters\': res.cpg_info.iter,\n')
    f.write('                    \'solve_time\': t1 - t0}\n')
    f.write(f'    prob._solver_stats = SolverStats(results_dict, \'{configuration.solver_name}\')\n\n')
    f.write('    return prob.value\n\n\n')
    
    f.write('def cpg_gradient(prob):\n\n')
    f.write('    # set variable deltas\n')
    f.write(f'    vdelta = cpg_module.{configuration.prefix}cpg_vdelta()\n')
    for name, size in variable_info.name_to_size.items():
        if size == 1:
            f.write(f'    vdelta.{name} = prob.var_dict["{name}"].gradient\n')
        else:
            f.write(f'    vdelta.{name} = list(prob.var_dict["{name}"].gradient.flatten(order="F"))\n')
    f.write('    pdelta = cpg_module.gradient(vdelta)\n')
    for name, shape in parameter_info.name_to_shape.items():
        if name in parameter_info.name_to_sparsity.keys():
            if parameter_info.name_to_sparsity_type[name] == 'diag':
                f.write(f'    {name}_coordinates_i = np.arange(n)\n')
                f.write(f'    {name}_coordinates_j = np.arange(n)\n')
            else:
                f.write(f'    {name}_coordinates_i = [c[0] for c in prob.param_dict["{name}"].attributes["sparsity"]]\n')
                f.write(f'    {name}_coordinates_j = [c[1] for c in prob.param_dict["{name}"].attributes["sparsity"]]\n')
            f.write(f'    prob.param_dict["{name}"].gradient = np.zeros(prob.param_dict["{name}"].shape)\n')
            f.write(f'    prob.param_dict["{name}"].gradient[{name}_coordinates_i, {name}_coordinates_j] = pdelta.{name}\n')
        else:
            if len(shape) == 2:
                f.write(f'    prob.param_dict[\'{name}\'].gradient = np.array(pdelta.{name}).reshape(({shape[0]}, {shape[1]}), order=\'F\')\n')
            elif len(shape) == 1:
                f.write(f'    prob.param_dict[\'{name}\'].gradient = np.array(pdelta.{name}).reshape({shape[0]})\n')
            else:
                f.write(f'    prob.param_dict[\'{name}\'].gradient = np.array(pdelta.{name})\n')


def replace_html_data(text, configuration, variable_info, dual_variable_info, parameter_info, solver_interface):
    """
    Replace placeholder strings in html documentation file
    """

    # description
    now = datetime.now()
    text = text.replace('$DATE', now.strftime("on %B %d, %Y at %H:%M:%S"))

    # param summary
    text = text.replace('$PARAMS', write_problem_summary(parameter_info.name_to_shape, parameter_info.name_to_size_usp))

    # primal variable summary
    text = text.replace('$PRIMALS', write_problem_summary(variable_info.name_to_shape, variable_info.name_to_size))

    # dual variable summary
    text = text.replace('$DUALS', write_problem_summary(dual_variable_info.name_to_shape, dual_variable_info.name_to_size))

    # code_dir
    text = text.replace('$CODEDIR', configuration.code_dir)
    text = text.replace('$CDPYTHON', configuration.code_dir.replace('/', '.').replace('\\', '.'))

    # solver name and docu
    text = text.replace('$CPGSOLVERNAME', configuration.solver_name)
    text = text.replace('$CPGSOLVERDOCUURL', solver_interface.docu)

    # CMake prefix
    text = text.replace('$CPGCMAKELISTS', configuration.prefix+'cpg')

    # type definition of CPG_Prim_t
    CPGPRIMTYPEDEF = '\n// Struct type with primal solution\n'
    CPGPRIMTYPEDEF += 'typedef struct {\n'
    for name, var in variable_info.name_to_init.items():
        if is_mathematical_scalar(var):
            s = ''
        else:
            s = '*'
        CPGPRIMTYPEDEF += (f'  cpg_float    {(s + name + ";").ljust(9)}   // Your variable {name}\n')
    CPGPRIMTYPEDEF += '} CPG_Prim_t;\n'
    text = text.replace('$CPGPRIMTYPEDEF', CPGPRIMTYPEDEF)

    # type definition of CPG_Dual_t
    if len(dual_variable_info.name_to_init) > 0:
        CPGDUALTYPEDEF = '\n// Struct type with dual solution\n'
        CPGDUALTYPEDEF += 'typedef struct {\n'
        for name, var in dual_variable_info.name_to_init.items():
            if is_mathematical_scalar(var):
                s = ''
            else:
                s = '*'
            CPGDUALTYPEDEF += (f'  cpg_float    {(s + name + ";").ljust(9)}   // Your dual variable for constraint {name}\n')
        CPGDUALTYPEDEF += '} CPG_Dual_t;\n\n'
    else:
        CPGDUALTYPEDEF = ''
    text = text.replace('$CPGDUALTYPEDEF', CPGDUALTYPEDEF)

    # type definition of CPG_Info_t
    CPGINFOTYPEDEF = '// Struct type with canonical solver information\n'
    CPGINFOTYPEDEF += 'typedef struct {\n'
    CPGINFOTYPEDEF += '  cpg_float    obj_val;    // Objective function value\n'
    CPGINFOTYPEDEF += '  cpg_int      iter;       // Number of iterations\n'
    CPGINFOTYPEDEF += (f'  {"cpg_int      " if solver_interface.status_is_int else "char       *"}status;     // Solver status\n')
    CPGINFOTYPEDEF += '  cpg_float    pri_res;    // Primal residual\n'
    CPGINFOTYPEDEF += '  cpg_float    dua_res;    // Dual residual\n'
    CPGINFOTYPEDEF += '} CPG_Info_t;\n'
    text = text.replace('$CPGINFOTYPEDEF', CPGINFOTYPEDEF)

    # type definition of CPG_Result_t
    CPGRESULTTYPEDEF = '\n// Struct type with user-defined objective value and solution as fields\n'
    CPGRESULTTYPEDEF += 'typedef struct {\n'
    CPGRESULTTYPEDEF += '  CPG_Prim_t *prim;      // Primal solution\n'
    if len(dual_variable_info.name_to_init) > 0:
        CPGRESULTTYPEDEF += '  CPG_Dual_t *dual;      // Dual solution\n'
    CPGRESULTTYPEDEF += '  CPG_Info_t *info;      // Solver information\n'
    CPGRESULTTYPEDEF += '} CPG_Result_t;\n'
    text = text.replace('$CPGRESULTTYPEDEF', CPGRESULTTYPEDEF)

    # update declarations
    CPGUPDATEDECLARATIONS = '\n// Update user-defined parameter values\n'
    for name, size in parameter_info.name_to_size_usp.items():
        if size == 1:
            CPGUPDATEDECLARATIONS += f'void {configuration.prefix}cpg_update_{name}(cpg_float value);\n'
        else:
            CPGUPDATEDECLARATIONS += f'void {configuration.prefix}cpg_update_{name}(cpg_int idx, cpg_float value);\n'
    text = text.replace('$CPGUPDATEDECLARATIONS', CPGUPDATEDECLARATIONS)

    # solve declarations
    CPGSOLVEDECLARATIONS = '\n// Solve via canonicalization, canonical solve, retrieval\n'
    CPGSOLVEDECLARATIONS += f'void {configuration.prefix}cpg_solve();\n'
    text = text.replace('$CPGSOLVEDECLARATIONS', CPGSOLVEDECLARATIONS)

    # settings declarations
    CPGSETTINGSDECLARATIONS = '\n// Update solver settings\n'
    CPGSETTINGSDECLARATIONS += f'void {configuration.prefix}cpg_set_solver_default_settings();\n'
    CPGSETTINGSDECLARATIONS += f'void {configuration.prefix}cpg_set_solver_&lt;setting_name&gt;(&lt;setting_type&gt; &lt;setting_name&gt;_new);\n'
    CPGSETTINGSDECLARATIONS += '...\n'
    text = text.replace('$CPGSETTINGSDECLARATIONS', CPGSETTINGSDECLARATIONS)

    # settings list
    CPGSETTINGSLIST = ', '.join([f'<code>{s}</code>' for s in solver_interface.stgs_names_enabled])

    return text.replace('$CPGSETTINGSLIST', CPGSETTINGSLIST)
