"""
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
from osqp.codegen import utils as osqp_utils


class C:
    CODE_DIR = 'code_dir'
    SOLVER_NAME = 'solver_name'
    UNROLL = 'unroll'
    PREFIX = 'prefix'
    RET_PRIM_FUNC_EXISTS = 'ret_prim_func_exists'
    RET_DUAL_FUNC_EXISTS = 'ret_dual_func_exists'
    NONZERO_D = 'nonzero_d'
    IS_MAXIMIZATION = 'is_maximization'
    P_WRITABLE = 'p_writable'
    P_FLAT_USP = 'p_flat_usp'
    P_COL_TO_NAME_USP = 'p_col_to_name_usp'
    P_NAME_TO_SHAPE = 'p_name_to_shape'
    P_NAME_TO_SIZE = 'p_name_to_size_usp'
    P_NAME_TO_CANON_OUTDATED = 'p_name_to_canon_outdated'
    P_NAME_TO_SPARSITY = 'p_name_to_sparsity'
    P_NAME_TO_SPARSITY_TYPE = 'p_name_to_sparsity_type'
    V_NAME_TO_INDICES = 'v_name_to_indices'
    V_NAME_TO_SIZE = 'v_name_to_size'
    V_NAME_TO_SHAPE = 'v_name_to_shape'
    V_NAME_TO_INIT = 'v_name_to_init'
    V_NAME_TO_SYM = 'v_name_to_sym'
    V_NAME_TO_OFFSET = 'v_name_to_offset'
    D_NAME_TO_INIT = 'd_name_to_init'
    D_NAME_TO_VEC = 'd_name_to_vec'
    D_NAME_TO_OFFSET = 'd_name_to_offset'
    D_NAME_TO_SIZE = 'd_name_to_size'
    D_NAME_TO_SHAPE = 'd_name_to_shape'
    D_NAME_TO_INDICES = 'd_name_to_indices'
    P = 'p'
    P_ID_TO_SIZE = 'p_id_to_size'
    P_ID_TO_CHANGES = 'p_id_to_changes'
    P_ID_TO_MAPPING = 'p_id_to_mapping'
    CONSTANTS = 'constants'
    SETTINGS_NAMES_TO_TYPE = 'settings_names_to_type'
    SETTINGS_NAMES_TO_DEFAULT = 'settings_names_to_default'


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('\n%s\n' % comment_str)
    f.write('Auto-generated by CVXPYgen %s.\n' % now.strftime("on %B %d, %Y at %H:%M:%S"))
    f.write('Content: %s.\n' % content)
    f.write('%s\n\n' % comment_str[::-1])


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 csc_to_dict(m):
    """
    Convert scipy csc matrix to dict that can be passed to osqp_utils.write_mat()
    """

    d = dict()
    d['i'] = m.indices
    d[C.P] = m.indptr
    d['x'] = m.data
    d['nzmax'] = m.nnz
    (d['m'], d['n']) = m.shape
    d['nz'] = -1

    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 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 = '%d by %d (%d)' % (sh[0], sh[1], name_to_size[n])
        string += '      <tr>\n        <td><code>%s</code></td>\n        <td>%s</td>\n      </tr>\n' % (n, shape_str)
    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 = '(%s%sCPG_Params.%s)+' % (sign_to_str[datum], prefix, user_name)
                        else:
                            ex = '(%.20f*%sCPG_Params.%s)+' % (datum, prefix, user_name)
                    else:
                        if abs(datum) == 1:
                            ex = '(%s%sCPG_Params.%s[%d])+' % (sign_to_str[datum], prefix, user_name, col - user_p_col)
                        else:
                            ex = '(%.20f*%sCPG_Params.%s[%d])+' % (datum, prefix, user_name, col - user_p_col)
                    break
            expr += ex
        expr = expr[:-1]
        if data.size > 0 and expr_is_const is False:
            if p_id == 'd':
                f.write('  %sCanon_Params.d = %s;\n' % (prefix, expr))
            else:
                f.write('  %sCanon_Params.%s%s[%d] = %s;\n' % (prefix, p_id, s, row, expr))


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

    f.write('  for(i=0; i<%d; i++){\n' % mapping.shape[0])
    f.write('    %sCanon_Params.%s%s[i] = 0;\n' % (prefix, canon_name, s))
    f.write('    for(j=%scanon_%s_map.p[i]; j<%scanon_%s_map.p[i+1]; j++){\n' %
            (prefix, canon_name, prefix, canon_name))
    f.write('      %sCanon_Params.%s%s[i] += %scanon_%s_map.x[j]*%scpg_params_vec[%scanon_%s_map.i[j]];\n' %
            (prefix, canon_name, s, prefix, canon_name, prefix, prefix, canon_name))
    f.write('    }\n')
    f.write('  }\n')


def write_param_def(f, param, name, prefix, suffix):
    """
    Use osqp.codegen.utils for writing vectors and matrices
    """
    if not param_is_empty(param):
        if name.isupper():
            osqp_utils.write_mat(f, param, '%scanon_%s%s' % (prefix, name, suffix))
        elif name == 'd':
            f.write('c_float %scanon_d%s = %.20f;\n' % (prefix, suffix, param[0]))
        else:
            osqp_utils.write_vec(f, param, '%scanon_%s%s' % (prefix, name, suffix), 'c_float')
        f.write('\n')


def write_param_prot(f, param, name, prefix, suffix):
    """
    Use osqp.codegen.utils for writing vectors and matrices
    """
    if not param_is_empty(param):
        if name.isupper():
            osqp_utils.write_mat_extern(f, param, '%scanon_%s%s' % (prefix, name, suffix))
        elif name == 'd':
            f.write('extern c_float %scanon_d%s;\n' % (prefix, suffix))
        else:
            osqp_utils.write_vec_extern(f, param, '%scanon_%s%s' % (prefix, name, suffix), 'c_float')


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

    f.write('c_float %s[%d] = {\n' % (name, mat.size))

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

    f.write('};\n')


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

    f.write("extern c_float cpg_%s[%d];\n" % (name, mat.size))


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

    f.write('%s %s = {\n' % (typ, name))

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

    f.write('};\n')


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

    f.write("extern %s %s;\n" % (typ, name))


def write_ecos_setup_update(f, canon_constants, prefix):
    """
    Write ECOS setup function to file
    """
    n = canon_constants['n']
    m = canon_constants['m']
    p = canon_constants[C.P]
    ell = canon_constants['l']
    n_cones = canon_constants['n_cones']
    e = canon_constants['e']

    if p == 0:
        Ax_str = Ap_str = Ai_str = b_str = '0'
    else:
        Ax_str = '%sCanon_Params_ECOS.A->x' % prefix
        Ap_str = '%sCanon_Params_ECOS.A->p' % prefix
        Ai_str = '%sCanon_Params_ECOS.A->i' % prefix
        b_str = '%sCanon_Params_ECOS.b' % prefix

    if n_cones == 0:
        ecos_q_str = '0'
    else:
        ecos_q_str = '(int *) &%secos_q' % prefix

    f.write('  if (!initialized) {\n')
    f.write('    %secos_workspace = ECOS_setup(%d, %d, %d, %d, %d, %s, %d, '
            '%sCanon_Params_ECOS.G->x, %sCanon_Params_ECOS.G->p, %sCanon_Params_ECOS.G->i, '
            '%s, %s, %s, %sCanon_Params_ECOS.c, %sCanon_Params_ECOS.h, %s);\n' %
            (prefix, n, m, p, ell, n_cones, ecos_q_str, e, prefix, prefix, prefix, Ax_str, Ap_str, Ai_str,
             prefix, prefix, b_str))
    f.write('    initialized = 1;\n')
    f.write('  } else {\n')
    f.write('    if (%sCanon_Outdated.G || %sCanon_Outdated.A || %sCanon_Outdated.b) {\n' % (prefix, prefix, prefix))
    f.write('      ECOS_updateData(%secos_workspace, %sCanon_Params_ECOS.G->x, %s, %sCanon_Params_ECOS.c, '
            '%sCanon_Params_ECOS.h, %s);\n' % (prefix, prefix, Ax_str, prefix, prefix, b_str))
    f.write('    } else {\n')
    f.write('      if (%sCanon_Outdated.h) {\n' % prefix)
    f.write('        for (i=0; i<%d; i++){\n' % m)
    f.write('          ecos_updateDataEntry_h(%secos_workspace, i, %sCanon_Params_ECOS.h[i]);\n' % (prefix, prefix))
    f.write('        }\n')
    f.write('      }\n')
    f.write('      if (%sCanon_Outdated.c) {\n' % prefix)
    f.write('        for (i=0; i<%d; i++){\n' % n)
    f.write('          ecos_updateDataEntry_c(%secos_workspace, i, %sCanon_Params_ECOS.c[i]);\n' % (prefix, prefix))
    f.write('        }\n')
    f.write('      }\n')
    f.write('    }\n')
    f.write('  }\n')


def write_workspace_def(f, info_opt, info_usr, info_can):

    write_description(f, 'c', 'Variable definitions')
    f.write('#include "cpg_workspace.h"\n')
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('#include "workspace.h"\n')

    if info_opt[C.UNROLL]:
        f.write('\n// User-defined parameters\n')
        user_casts = []
        user_values = []
        names = list(info_usr[C.P_WRITABLE].keys())
        for name in names:
            value = info_usr[C.P_WRITABLE][name]
            if np.isscalar(value):
                user_casts.append('')
                user_values.append('%.20f' % value)
            else:
                osqp_utils.write_vec(f, value, info_opt[C.PREFIX] + 'cpg_' + name, 'c_float')
                f.write('\n')
                user_casts.append('(c_float *) ')
                user_values.append('&' + info_opt[C.PREFIX] + 'cpg_' + name)
        f.write('// Struct containing all user-defined parameters\n')
        write_struct_def(f, names, user_casts, user_values, '%sCPG_Params' % info_opt[C.PREFIX], 'CPG_Params_t')
        f.write('\n')
    else:
        f.write('\n// Vector containing flattened user-defined parameters\n')
        osqp_utils.write_vec(f, info_usr[C.P_FLAT_USP], '%scpg_params_vec' % info_opt[C.PREFIX], 'c_float')
        f.write('\n// Sparse mappings from user-defined to canonical parameters\n')
        for p_id, mapping in info_can[C.P_ID_TO_MAPPING].items():
            if info_can[C.P_ID_TO_CHANGES][p_id]:
                osqp_utils.write_mat(f, csc_to_dict(mapping), '%scanon_%s_map' % (info_opt[C.PREFIX], p_id))
                f.write('\n')

    p_ids = list(info_can[C.P].keys())
    canon_casts = []
    f.write('// Canonical parameters\n')
    for p_id in p_ids:
        p = info_can[C.P][p_id]
        if p_id == 'd':
            canon_casts.append('')
        else:
            write_param_def(f, replace_inf(p), p_id, info_opt[C.PREFIX], '')
            if info_opt[C.SOLVER_NAME] == 'ECOS':
                write_param_def(f, replace_inf(p), p_id, info_opt[C.PREFIX], '_ECOS')
            if p_id.isupper():
                canon_casts.append('')
            else:
                canon_casts.append('(c_float *) ')

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

    struct_values = []
    struct_values_ECOS = []
    for i, p_id in enumerate(p_ids):
        p = info_can[C.P][p_id]
        if type(p) == dict:
            length = len(p['x'])
        else:
            length = len(p)
        if length == 0:
            struct_values.append('0')
            if info_opt[C.SOLVER_NAME] == 'ECOS':
                struct_values_ECOS.append('0')
        elif length == 1:
            struct_values.append('%.20f' % p)
            if info_opt[C.SOLVER_NAME] == 'ECOS':
                struct_values_ECOS.append('%.20f' % p)
        else:
            struct_values.append('&%scanon_%s' % (info_opt[C.PREFIX], p_id))
            if info_opt[C.SOLVER_NAME] == 'ECOS':
                struct_values_ECOS.append('&%scanon_%s_ECOS' % (info_opt[C.PREFIX], p_id))

    write_struct_def(f, p_ids, canon_casts, struct_values, '%sCanon_Params' % info_opt[C.PREFIX], 'Canon_Params_t')
    f.write('\n')
    if info_opt[C.SOLVER_NAME] == 'ECOS':
        write_struct_def(f, p_ids, canon_casts, struct_values_ECOS, '%sCanon_Params_ECOS' % info_opt[C.PREFIX],
                         'Canon_Params_t')
        f.write('\n')

    # Boolean struct for outdated parameter flags
    f.write('// Struct containing flags for outdated canonical parameters\n')
    f.write('Canon_Outdated_t %sCanon_Outdated = {\n' % info_opt[C.PREFIX])
    for p_id in info_can[C.P].keys():
        f.write('.%s = 0,\n' % p_id)
    f.write('};\n\n')

    prim_cast = []
    if any(info_usr[C.V_NAME_TO_SYM]) or info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('// User-defined variables\n')
    for name, value in info_usr[C.V_NAME_TO_INIT].items():
        if np.isscalar(value):
            prim_cast.append('')
        else:
            prim_cast.append('(c_float *) ')
            if info_usr[C.V_NAME_TO_SYM][name] or info_opt[C.SOLVER_NAME] == 'ECOS':
                osqp_utils.write_vec(f, value.flatten(order='F'), info_opt[C.PREFIX] + name, 'c_float')
                f.write('\n')

    f.write('// Struct containing primal solution\n')
    CPG_Prim_fields = list(info_usr[C.V_NAME_TO_INIT].keys())
    CPG_Prim_values = []
    for name, var in info_usr[C.V_NAME_TO_INIT].items():
        offset = info_usr[C.V_NAME_TO_OFFSET][name]
        if np.isscalar(var):
            CPG_Prim_values.append('0')
        else:
            if info_usr[C.V_NAME_TO_SYM][name] or info_opt[C.SOLVER_NAME] == 'ECOS':
                CPG_Prim_values.append('&' + info_opt[C.PREFIX] + name)
            else:
                if info_opt[C.SOLVER_NAME] == 'OSQP':
                    CPG_Prim_values.append('&xsolution + %d' % offset)
                elif info_opt[C.SOLVER_NAME] == 'SCS':
                    CPG_Prim_values.append('&%sscs_x + %d' % (info_opt[C.PREFIX], offset))
    write_struct_def(f, CPG_Prim_fields, prim_cast, CPG_Prim_values, '%sCPG_Prim' % info_opt[C.PREFIX], 'CPG_Prim_t')

    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        dual_cast = []
        if info_opt[C.SOLVER_NAME] == 'ECOS':
            f.write('\n// Dual variables associated with user-defined constraints\n')
        for name, value in info_usr[C.D_NAME_TO_INIT].items():
            if np.isscalar(value):
                dual_cast.append('')
            else:
                dual_cast.append('(c_float *) ')
                if info_opt[C.SOLVER_NAME] == 'ECOS':
                    osqp_utils.write_vec(f, value.flatten(order='F'), info_opt[C.PREFIX] + name, 'c_float')
                    f.write('\n')

        f.write('// Struct containing dual solution\n')
        CPG_Dual_fields = info_usr[C.D_NAME_TO_INIT].keys()
        CPG_Dual_values = []
        for name, var in info_usr[C.D_NAME_TO_INIT].items():
            vec = info_usr[C.D_NAME_TO_VEC][name]
            offset = info_usr[C.D_NAME_TO_OFFSET][name]
            if np.isscalar(var):
                CPG_Dual_values.append('0')
            else:
                if info_opt[C.SOLVER_NAME] == 'OSQP':
                    CPG_Dual_values.append('&%ssolution + %d' % (vec, offset))
                elif info_opt[C.SOLVER_NAME] == 'SCS':
                    CPG_Dual_values.append('&%sscs_%s + %d' % (info_opt[C.PREFIX], vec, offset))
                else:
                    CPG_Dual_values.append('&' + info_opt[C.PREFIX] + name)
        write_struct_def(f, CPG_Dual_fields, dual_cast, CPG_Dual_values, '%sCPG_Dual' % info_opt[C.PREFIX],
                         'CPG_Dual_t')

    f.write('\n// Struct containing solver info\n')
    CPG_Info_fields = ['obj_val', 'iter', 'status', 'pri_res', 'dua_res']
    if info_opt[C.SOLVER_NAME] in ['OSQP', 'SCS']:
        CPG_Info_values = ['0', '0', '"unknown"', '0', '0']
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        CPG_Info_values = ['0', '0', '0', '0', '0']
    else:
        raise ValueError("Problem class cannot be addressed by the OSQP or ECOS solver!")
    info_cast = ['', '', '', '', '']
    write_struct_def(f, CPG_Info_fields, info_cast, CPG_Info_values, '%sCPG_Info' % info_opt[C.PREFIX], 'CPG_Info_t')

    f.write('\n// Struct containing solution and info\n')
    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        CPG_Result_fields = ['prim', 'dual', 'info']
        result_cast = ['', '', '']
        CPG_Result_values = ['&%sCPG_Prim' % info_opt[C.PREFIX], '&%sCPG_Dual' % info_opt[C.PREFIX],
                             '&%sCPG_Info' % info_opt[C.PREFIX]]
    else:
        CPG_Result_fields = ['prim', 'info']
        result_cast = ['', '']
        CPG_Result_values = ['&%sCPG_Prim' % info_opt[C.PREFIX], '&%sCPG_Info' % info_opt[C.PREFIX]]
    write_struct_def(f, CPG_Result_fields, result_cast, CPG_Result_values, '%sCPG_Result' % info_opt[C.PREFIX],
                     'CPG_Result_t')

    if info_opt[C.SOLVER_NAME] == 'SCS':

        f.write('\n// SCS matrix A\n')
        scs_A_fiels = ['x', 'i', 'p', 'm', 'n']
        scs_A_casts = ['(c_float *) ', '(c_int *) ', '(c_int *) ', '', '']
        scs_A_values = ['&%scanon_A_x' % info_opt[C.PREFIX], '&%scanon_A_i' % info_opt[C.PREFIX],
                        '&%scanon_A_p' % info_opt[C.PREFIX], str(info_can[C.CONSTANTS]['m']),
                        str(info_can[C.CONSTANTS]['n'])]
        write_struct_def(f, scs_A_fiels, scs_A_casts, scs_A_values, '%sScs_A' % info_opt[C.PREFIX], 'ScsMatrix')

        f.write('\n// Struct containing SCS data\n')
        scs_d_fiels = ['m', 'n', 'A', 'P', 'b', 'c']
        scs_d_casts = ['', '', '', '', '(c_float *) ', '(c_float *) ']
        scs_d_values = [str(info_can[C.CONSTANTS]['m']), str(info_can[C.CONSTANTS]['n']), '&%sScs_A'
                        % info_opt[C.PREFIX], 'SCS_NULL', '&%scanon_b' % info_opt[C.PREFIX], '&%scanon_c'
                        % info_opt[C.PREFIX]]
        write_struct_def(f, scs_d_fiels, scs_d_casts, scs_d_values, '%sScs_D' % info_opt[C.PREFIX], 'ScsData')

        if info_can[C.CONSTANTS]['qsize'] > 0:
            f.write('\n// SCS array of SOC dimensions\n')
            osqp_utils.write_vec(f, info_can[C.CONSTANTS]['q'], '%sscs_q' % info_opt[C.PREFIX], 'c_int')
            k_field_q_str = '&%sscs_q' % info_opt[C.PREFIX]
        else:
            k_field_q_str = 'SCS_NULL'

        f.write('\n// Struct containing SCS cone data\n')
        scs_k_fields = ['z', 'l', 'bu', 'bl', 'bsize', 'q', 'qsize', 's', 'ssize', 'ep', 'ed', 'p', 'psize']
        scs_k_casts = ['', '', '(c_float *) ', '(c_float *) ', '', '(c_int *) ', '', '(c_int *) ', '', '', '',
                       '(c_float *) ', '']
        scs_k_values = [str(info_can[C.CONSTANTS]['z']), str(info_can[C.CONSTANTS]['l']), 'SCS_NULL', 'SCS_NULL', '0',
                        k_field_q_str, str(info_can[C.CONSTANTS]['qsize']), 'SCS_NULL', '0', '0', '0', 'SCS_NULL', '0']
        write_struct_def(f, scs_k_fields, scs_k_casts, scs_k_values, '%sScs_K' % info_opt[C.PREFIX], 'ScsCone')

        f.write('\n// Struct containing SCS settings\n')
        scs_stgs_fields = list(info_can[C.SETTINGS_NAMES_TO_DEFAULT].keys())
        scs_stgs_casts = ['']*len(scs_stgs_fields)
        scs_stgs_values = list(info_can[C.SETTINGS_NAMES_TO_DEFAULT].values())
        write_struct_def(f, scs_stgs_fields, scs_stgs_casts, scs_stgs_values, '%sScs_Stgs'
                         % info_opt[C.PREFIX], 'ScsSettings')

        f.write('\n// SCS solution\n')
        osqp_utils.write_vec(f, np.zeros(info_can[C.CONSTANTS]['n']), '%sscs_x' % info_opt[C.PREFIX], 'c_float')
        osqp_utils.write_vec(f, np.zeros(info_can[C.CONSTANTS]['m']), '%sscs_y' % info_opt[C.PREFIX], 'c_float')
        osqp_utils.write_vec(f, np.zeros(info_can[C.CONSTANTS]['m']), '%sscs_s' % info_opt[C.PREFIX], 'c_float')

        f.write('\n// Struct containing SCS solution\n')
        scs_sol_fields = ['x', 'y', 's']
        scs_sol_casts = ['(c_float *) ', '(c_float *) ', '(c_float *) ']
        scs_sol_values = ['&%sscs_x' % info_opt[C.PREFIX], '&%sscs_y' % info_opt[C.PREFIX], '&%sscs_s'
                          % info_opt[C.PREFIX]]
        write_struct_def(f, scs_sol_fields, scs_sol_casts, scs_sol_values, '%sScs_Sol'
                         % info_opt[C.PREFIX], 'ScsSolution')

        f.write('\n// Struct containing SCS information\n')
        scs_info_fields = ['iter', 'status', 'status_val', 'scale_updates', 'pobj', 'dobj', 'res_pri', 'res_dual',
                           'gap', 'res_infeas', 'res_unbdd_a', 'res_unbdd_p', 'comp_slack', 'setup_time', 'solve_time',
                           'scale', 'rejected_accel_steps', 'accepted_accel_steps', 'lin_sys_time', 'cone_time',
                           'accel_time']
        scs_info_casts = ['']*len(scs_info_fields)
        scs_info_values = ['0', '"unknown"', '0', '0', '0', '0', '99', '99', '99', '99', '99', '99', '99', '0', '0',
                           '1', '0', '0', '0', '0', '0']
        write_struct_def(f, scs_info_fields, scs_info_casts, scs_info_values, '%sScs_Info'
                         % info_opt[C.PREFIX], 'ScsInfo')

        f.write('\n// Pointer to struct containing SCS workspace\n')
        f.write('ScsWork* %sScs_Work = 0;\n' % info_opt[C.PREFIX])

    if info_opt[C.SOLVER_NAME] == 'ECOS':

        f.write('\n// Struct containing solver settings\n')
        f.write('Canon_Settings_t %sCanon_Settings = {\n' % info_opt[C.PREFIX])
        for name, default in info_can[C.SETTINGS_NAMES_TO_DEFAULT].items():
            f.write('.%s = %s,\n' % (name, default))
        f.write('};\n')
        if info_can[C.CONSTANTS]['n_cones'] > 0:
            f.write('\n// ECOS array of SOC dimensions\n')
            osqp_utils.write_vec(f, info_can[C.CONSTANTS]['q'], '%secos_q' % info_opt[C.PREFIX], 'c_int')
        f.write('\n// ECOS workspace\n')
        f.write('pwork* %secos_workspace = 0;\n' % info_opt[C.PREFIX])
        f.write('\n// ECOS exit flag\n')
        f.write('c_int %secos_flag = -99;\n' % info_opt[C.PREFIX])


def write_workspace_prot(f, info_opt, info_usr, info_can):
    """"
    Write workspace initialization to file
    """

    write_description(f, 'c', 'Type definitions and variable declarations')
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('#include "types.h"\n\n')
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('#include "scs.h"\n\n')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('#include "ecos.h"\n\n')

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

    if info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('typedef scs_float c_float;\n')
        f.write('typedef scs_int c_int;\n\n')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('typedef double c_float;\n')
        f.write('typedef int c_int;\n\n')

    # struct definitions
    if info_opt[C.SOLVER_NAME] in ['SCS', 'ECOS']:
        f.write('// Compressed sparse column (csc) matrix\n')
        f.write('typedef struct {\n')
        f.write('  c_int      nzmax;\n')
        f.write('  c_int      n;\n')
        f.write('  c_int      m;\n')
        f.write('  c_int      *p;\n')
        f.write('  c_int      *i;\n')
        f.write('  c_float    *x;\n')
        f.write('  c_int      nz;\n')
        f.write('} csc;\n\n')

    if info_opt[C.UNROLL]:
        f.write('// User-defined parameters\n')
        f.write('typedef struct {\n')
        # single user parameters
        for name, size in info_usr[C.P_NAME_TO_SIZE].items():
            if size == 1:
                s = ''
            else:
                s = '*'
            f.write('  c_float    %s   // Your parameter %s\n' % ((s+name+';').ljust(9), name))
        f.write('} CPG_Params_t;\n\n')

    f.write('// Canonical parameters\n')
    f.write('typedef struct {\n')
    for p_id in info_can[C.P].keys():
        if p_id.isupper():
            f.write('  csc        *%s   // Canonical parameter %s\n' % ((p_id+';').ljust(8), p_id))
        else:
            if p_id == 'd':
                s = ''
            else:
                s = '*'
            f.write('  c_float    %s   // Canonical parameter %s\n' % ((s+p_id+';').ljust(9), p_id))
    f.write('} Canon_Params_t;\n\n')

    f.write('// Flags indicating outdated canonical parameters\n')
    f.write('typedef struct {\n')
    for p_id in info_can[C.P].keys():
        f.write('  int        %s    // Bool, if canonical parameter %s outdated\n' % ((p_id + ';').ljust(8), p_id))
    f.write('} Canon_Outdated_t;\n\n')

    f.write('// Primal solution\n')
    f.write('typedef struct {\n')
    for name, var in info_usr[C.V_NAME_TO_INIT].items():
        if np.isscalar(var):
            s = ''
        else:
            s = '*'
        f.write('  c_float    %s   // Your variable %s\n' % ((s + name + ';').ljust(9), name))
    f.write('} CPG_Prim_t;\n\n')

    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('// Dual solution\n')
        f.write('typedef struct {\n')
        for name, var in info_usr[C.D_NAME_TO_INIT].items():
            if np.isscalar(var):
                s = ''
            else:
                s = '*'
            f.write('  c_float    %s   // Your dual variable for constraint %s\n' % ((s + name + ';').ljust(9), name))
        f.write('} CPG_Dual_t;\n\n')

    f.write('// Solver information\n')
    f.write('typedef struct {\n')
    f.write('  c_float    obj_val;    // Objective function value\n')
    f.write('  c_int      iter;       // Number of iterations\n')
    if info_opt[C.SOLVER_NAME] in ['OSQP', 'SCS']:
        f.write('  char       *status;    // Solver status\n')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('  c_int      status;     // Solver status\n')
    f.write('  c_float    pri_res;    // Primal residual\n')
    f.write('  c_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(info_usr[C.D_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')

    if info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('// Solver settings\n')
        f.write('typedef struct {\n')
        for name, typ in info_can[C.SETTINGS_NAMES_TO_TYPE].items():
            f.write('  %s%s;\n' % (typ.ljust(11), name))
        f.write('} Canon_Settings_t;\n\n')

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

    if info_opt[C.UNROLL]:
        f.write('\n// User-defined parameters\n')
        for name, value in info_usr[C.P_WRITABLE].items():
            if not np.isscalar(value):
                osqp_utils.write_vec_extern(f, value, info_opt[C.PREFIX]+'cpg_'+name, 'c_float')
        f.write('\n// Struct containing all user-defined parameters\n')
        write_struct_prot(f, '%sCPG_Params' % info_opt[C.PREFIX], 'CPG_Params_t')
    else:
        f.write('\n// Vector containing flattened user-defined parameters\n')
        osqp_utils.write_vec_extern(f, info_usr[C.P_FLAT_USP], '%scpg_params_vec' % info_opt[C.PREFIX], 'c_float')
        f.write('\n// Sparse mappings from user-defined to canonical parameters\n')
        for p_id, mapping in info_can[C.P_ID_TO_MAPPING].items():
            if info_can[C.P_ID_TO_CHANGES][p_id]:
                osqp_utils.write_mat_extern(f, csc_to_dict(mapping), '%scanon_%s_map' % (info_opt[C.PREFIX], p_id))

    f.write('\n// Canonical parameters\n')
    for p_id, p in info_can[C.P].items():
        if p_id != 'd':
            write_param_prot(f, p, p_id, info_opt[C.PREFIX], '')
            if info_opt[C.SOLVER_NAME] == 'ECOS':
                write_param_prot(f, p, p_id, info_opt[C.PREFIX], '_ECOS')

    f.write('\n// Struct containing canonical parameters\n')
    write_struct_prot(f, '%sCanon_Params' % info_opt[C.PREFIX], 'Canon_Params_t')
    if info_opt[C.SOLVER_NAME] == 'ECOS':
        write_struct_prot(f, '%sCanon_Params_ECOS' % info_opt[C.PREFIX], 'Canon_Params_t')

    f.write('\n// Struct containing flags for outdated canonical parameters\n')
    f.write('extern Canon_Outdated_t %sCanon_Outdated;\n' % info_opt[C.PREFIX])

    if any(info_usr[C.V_NAME_TO_SYM].values()) or info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('\n// User-defined variables\n')
        for name, value in info_usr[C.V_NAME_TO_INIT].items():
            if info_usr[C.V_NAME_TO_SYM][name] or info_opt[C.SOLVER_NAME] == 'ECOS':
                if not np.isscalar(value):
                    osqp_utils.write_vec_extern(f, value.flatten(order='F'), info_opt[C.PREFIX]+'cpg_'+name,
                                                'c_float')

    if info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('\n// Dual variables associated with user-defined constraints\n')
        for name, value in info_usr[C.D_NAME_TO_INIT].items():
            if not np.isscalar(value):
                osqp_utils.write_vec_extern(f, value.flatten(order='F'), info_opt[C.PREFIX]+'cpg_'+name, 'c_float')

    f.write('\n// Struct containing primal solution\n')
    write_struct_prot(f, '%sCPG_Prim' % info_opt[C.PREFIX], 'CPG_Prim_t')

    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('\n// Struct containing dual solution\n')
        write_struct_prot(f, '%sCPG_Dual' % info_opt[C.PREFIX], 'CPG_Dual_t')

    f.write('\n// Struct containing solver info\n')
    write_struct_prot(f, '%sCPG_Info' % info_opt[C.PREFIX], 'CPG_Info_t')

    f.write('\n// Struct containing solution and info\n')
    write_struct_prot(f, '%sCPG_Result' % info_opt[C.PREFIX], 'CPG_Result_t')

    if info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('\n// SCS matrix A\n')
        write_struct_prot(f, '%sscs_A' % info_opt[C.PREFIX], 'ScsMatrix')
        f.write('\n// Struct containing SCS data\n')
        write_struct_prot(f, '%sScs_D' % info_opt[C.PREFIX], 'ScsData')
        if info_can[C.CONSTANTS]['qsize'] > 0:
            f.write('\n// SCS array of SOC dimensions\n')
            osqp_utils.write_vec_extern(f, info_can[C.CONSTANTS]['q'], '%sscs_q' % info_opt[C.PREFIX], 'c_int')
        f.write('\n// Struct containing SCS cone data\n')
        write_struct_prot(f, '%sScs_K' % info_opt[C.PREFIX], 'ScsCone')
        f.write('\n// Struct containing SCS settings\n')
        write_struct_prot(f, '%sScs_Stgs' % info_opt[C.PREFIX], 'ScsSettings')
        f.write('\n// SCS solution\n')
        osqp_utils.write_vec_extern(f, np.zeros(info_can[C.CONSTANTS]['n']), '%sscs_x' % info_opt[C.PREFIX],
                                    'c_float')
        osqp_utils.write_vec_extern(f, np.zeros(info_can[C.CONSTANTS]['m']), '%sscs_y' % info_opt[C.PREFIX],
                                    'c_float')
        osqp_utils.write_vec_extern(f, np.zeros(info_can[C.CONSTANTS]['m']), '%sscs_s' % info_opt[C.PREFIX],
                                    'c_float')
        f.write('\n// Struct containing SCS solution\n')
        write_struct_prot(f, '%sScs_Sol' % info_opt[C.PREFIX], 'ScsSolution')
        f.write('\n// Struct containing SCS information\n')
        write_struct_prot(f, '%sScs_Info' % info_opt[C.PREFIX], 'ScsInfo')
        f.write('\n// Pointer to struct containing SCS workspace\n')
        write_struct_prot(f, '%sScs_Work' % info_opt[C.PREFIX], 'ScsWork*')

    if info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('\n// Struct containing solver settings\n')
        write_struct_prot(f, '%sCanon_Settings' % info_opt[C.PREFIX], 'Canon_Settings_t')
        if info_can[C.CONSTANTS]['n_cones'] > 0:
            f.write('\n// ECOS array of SOC dimensions\n')
            osqp_utils.write_vec_extern(f, info_can[C.CONSTANTS]['q'], '%secos_q' % info_opt[C.PREFIX], 'c_int')
        f.write('\n// ECOS workspace\n')
        f.write('extern pwork* %secos_workspace;\n' % info_opt[C.PREFIX])
        f.write('\n// ECOS exit flag\n')
        f.write('extern c_int %secos_flag;\n' % info_opt[C.PREFIX])


def write_solve_def(f, info_opt, info_cg, info_usr, info_can):
    """
    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')
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('#include "workspace.h"\n')
        f.write('#include "osqp.h"\n\n')
    else:
        f.write('\n')

    if not info_opt[C.UNROLL]:
        f.write('static c_int i;\n')
        f.write('static c_int j;\n')

    if info_opt[C.UNROLL] and info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('static c_int i;\n')

    if info_opt[C.SOLVER_NAME] in ['SCS', 'ECOS']:
        f.write('static c_int initialized = 0;\n')

    f.write('\n// Update user-defined parameters\n')
    if info_opt[C.UNROLL]:
        for user_p_name, Canon_outdated_names in info_usr[C.P_NAME_TO_CANON_OUTDATED].items():
            if info_usr[C.P_NAME_TO_SIZE][user_p_name] == 1:
                f.write('void %scpg_update_%s(c_float val){\n' % (info_opt[C.PREFIX], user_p_name))
                f.write('  %sCPG_Params.%s = val;\n' % (info_opt[C.PREFIX], user_p_name))
            else:
                f.write('void %scpg_update_%s(c_int idx, c_float val){\n' % (info_opt[C.PREFIX], user_p_name))
                f.write('  %sCPG_Params.%s[idx] = val;\n' % (info_opt[C.PREFIX], user_p_name))
            for Canon_outdated_name in Canon_outdated_names:
                f.write('  %sCanon_Outdated.%s = 1;\n' % (info_opt[C.PREFIX], Canon_outdated_name))
            f.write('}\n\n')
    else:
        for base_col, name in info_usr[C.P_COL_TO_NAME_USP].items():
            Canon_outdated_names = info_usr[C.P_NAME_TO_CANON_OUTDATED][name]
            if info_usr[C.P_NAME_TO_SIZE][name] == 1:
                f.write('void %scpg_update_%s(c_float val){\n' % (info_opt[C.PREFIX], name))
                f.write('  %scpg_params_vec[%d] = val;\n' % (info_opt[C.PREFIX], base_col))
            else:
                f.write('void %scpg_update_%s(c_int idx, c_float val){\n' % (info_opt[C.PREFIX], name))
                f.write('  %scpg_params_vec[idx+%d] = val;\n' % (info_opt[C.PREFIX], base_col))
            for Canon_outdated_name in Canon_outdated_names:
                f.write('  %sCanon_Outdated.%s = 1;\n' % (info_opt[C.PREFIX], Canon_outdated_name))
            f.write('}\n\n')

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

    for p_id, mapping in info_can[C.P_ID_TO_MAPPING].items():
        if info_can[C.P_ID_TO_CHANGES][p_id]:
            f.write('void %scpg_canonicalize_%s(){\n' % (info_opt[C.PREFIX], p_id))
            if p_id.isupper():
                s = '->x'
            else:
                s = ''
            if info_opt[C.UNROLL]:
                write_canonicalize_explicit(f, p_id, s, mapping, info_usr[C.P_COL_TO_NAME_USP],
                                            info_usr[C.P_NAME_TO_SIZE], info_opt[C.PREFIX])
            else:
                write_canonicalize(f, p_id, s, mapping, info_opt[C.PREFIX])
            f.write('}\n\n')

    if info_opt[C.SOLVER_NAME] == 'OSQP':
        obj_str = 'workspace.info->obj_val'
        prim_str = 'workspace.solution->x'
        dual_str = 'workspace.solution->'
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        obj_str = '%sScs_Info.pobj' % info_opt[C.PREFIX]
        prim_str = '%sscs_x' % info_opt[C.PREFIX]
        dual_str = '%sscs_' % info_opt[C.PREFIX]
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        obj_str = '%secos_workspace->info->pcost' % info_opt[C.PREFIX]
        prim_str = '%secos_workspace->x' % info_opt[C.PREFIX]
        dual_str = '%secos_workspace->' % info_opt[C.PREFIX]
    else:
        raise ValueError("Only OSQP and ECOS are supported!")

    if info_cg[C.RET_PRIM_FUNC_EXISTS]:
        f.write('// Retrieve primal solution in terms of user-defined variables\n')
        f.write('void %scpg_retrieve_prim(){\n' % info_opt[C.PREFIX])
        for var_name, indices in info_usr[C.V_NAME_TO_INDICES].items():
            if len(indices) == 1:
                f.write('  %sCPG_Prim.%s = %s[%d];\n' % (info_opt[C.PREFIX], var_name, prim_str, indices))
            elif info_usr[C.V_NAME_TO_SYM][var_name] or info_opt[C.SOLVER_NAME] == 'ECOS':
                for i, idx in enumerate(indices):
                    f.write('  %sCPG_Prim.%s[%d] = %s[%d];\n' % (info_opt[C.PREFIX], var_name, i, prim_str, idx))
        f.write('}\n\n')

    if info_cg[C.RET_DUAL_FUNC_EXISTS]:
        f.write('// Retrieve dual solution in terms of user-defined constraints\n')
        f.write('void %scpg_retrieve_dual(){\n' % info_opt[C.PREFIX])
        for var_name, (vector, indices) in info_usr[C.D_NAME_TO_INDICES].items():
            if len(indices) == 1:
                f.write('  %sCPG_Dual.%s = %s%s[%d];\n' % (info_opt[C.PREFIX], var_name, dual_str, vector, indices))
            elif info_opt[C.SOLVER_NAME] == 'ECOS':
                for i, idx in enumerate(indices):
                    f.write('  %sCPG_Dual.%s[%d] = %s%s[%d];\n'
                            % (info_opt[C.PREFIX], var_name, i, dual_str, vector, idx))
        f.write('}\n\n')

    f.write('// Retrieve solver info\n')
    f.write('void %scpg_retrieve_info(){\n' % info_opt[C.PREFIX])
    if info_cg[C.NONZERO_D]:
        d_str = ' + *%sCanon_Params.d' % info_opt[C.PREFIX]
    else:
        d_str = ''
    if info_cg[C.IS_MAXIMIZATION]:
        f.write('  %sCPG_Info.obj_val = -(%s%s);\n' % (info_opt[C.PREFIX], obj_str, d_str))
    else:
        f.write('  %sCPG_Info.obj_val = %s%s;\n' % (info_opt[C.PREFIX], obj_str, d_str))
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('  %sCPG_Info.iter = workspace.info->iter;\n' % info_opt[C.PREFIX])
        f.write('  %sCPG_Info.status = workspace.info->status;\n' % info_opt[C.PREFIX])
        f.write('  %sCPG_Info.pri_res = workspace.info->pri_res;\n' % info_opt[C.PREFIX])
        f.write('  %sCPG_Info.dua_res = workspace.info->dua_res;\n' % info_opt[C.PREFIX])
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('  %sCPG_Info.iter = %sScs_Info.iter;\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  %sCPG_Info.status = %sScs_Info.status;\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  %sCPG_Info.pri_res = %sScs_Info.res_pri;\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  %sCPG_Info.dua_res = %sScs_Info.res_dual;\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('  %sCPG_Info.iter = %secos_workspace->info->iter;\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  %sCPG_Info.status = %secos_flag;\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  %sCPG_Info.pri_res = %secos_workspace->info->pres;\n'
                % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  %sCPG_Info.dua_res = %secos_workspace->info->dres;\n'
                % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    f.write('}\n\n')

    f.write('// Solve via canonicalization, canonical solve, retrieval\n')
    f.write('void %scpg_solve(){\n' % info_opt[C.PREFIX])
    f.write('  // Canonicalize if necessary\n')
    if info_opt[C.SOLVER_NAME] == 'OSQP':

        if info_can[C.P_ID_TO_CHANGES]['P'] and info_can[C.P_ID_TO_CHANGES]['A']:
            f.write('  if (%sCanon_Outdated.P && %sCanon_Outdated.A) {\n'
                    % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
            f.write('    %scpg_canonicalize_P();\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_A();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_P_A(&workspace, %sCanon_Params.P->x, 0, 0, %sCanon_Params.A->x, 0, 0);\n'
                    % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
            f.write('  } else if (%sCanon_Outdated.P) {\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_P();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_P(&workspace, %sCanon_Params.P->x, 0, 0);\n' % info_opt[C.PREFIX])
            f.write('  } else if (%sCanon_Outdated.A) {\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_A();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_A(&workspace, %sCanon_Params.A->x, 0, 0);\n' % info_opt[C.PREFIX])
            f.write('  }\n')
        else:
            if info_can[C.P_ID_TO_CHANGES]['P']:
                f.write('  if (%sCanon_Outdated.P) {\n' % info_opt[C.PREFIX])
                f.write('    %scpg_canonicalize_P();\n' % info_opt[C.PREFIX])
                f.write('    osqp_update_P(&workspace, %sCanon_Params.P->x, 0, 0);\n' % info_opt[C.PREFIX])
                f.write('  }\n')
            if info_can[C.P_ID_TO_CHANGES]['A']:
                f.write('  if (%sCanon_Outdated.A) {\n' % info_opt[C.PREFIX])
                f.write('    %scpg_canonicalize_A();\n' % info_opt[C.PREFIX])
                f.write('    osqp_update_A(&workspace, %sCanon_Params.A->x, 0, 0);\n' % info_opt[C.PREFIX])
                f.write('  }\n')

        if info_can[C.P_ID_TO_CHANGES]['q']:
            f.write('  if (%sCanon_Outdated.q) {\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_q();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_lin_cost(&workspace, %sCanon_Params.q);\n' % info_opt[C.PREFIX])
            f.write('  }\n')

        if info_can[C.P_ID_TO_CHANGES]['d']:
            f.write('  if (%sCanon_Outdated.d) {\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_d();\n' % info_opt[C.PREFIX])
            f.write('  }\n')

        if info_can[C.P_ID_TO_CHANGES]['l'] and info_can[C.P_ID_TO_CHANGES]['u']:
            f.write('  if (%sCanon_Outdated.l && %sCanon_Outdated.u) {\n'
                    % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
            f.write('    %scpg_canonicalize_l();\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_u();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_bounds(&workspace, %sCanon_Params.l, %sCanon_Params.u);\n'
                    % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
            f.write('  } else if (%sCanon_Outdated.l) {\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_l();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_lower_bound(&workspace, %sCanon_Params.l);\n' % info_opt[C.PREFIX])
            f.write('  } else if (%sCanon_Outdated.u) {\n' % info_opt[C.PREFIX])
            f.write('    %scpg_canonicalize_u();\n' % info_opt[C.PREFIX])
            f.write('    osqp_update_upper_bound(&workspace, %sCanon_Params.u);\n' % info_opt[C.PREFIX])
            f.write('  }\n')
        else:
            if info_can[C.P_ID_TO_CHANGES]['l']:
                f.write('  if (%sCanon_Outdated.l) {\n' % info_opt[C.PREFIX])
                f.write('    %scpg_canonicalize_l();\n' % info_opt[C.PREFIX])
                f.write('    osqp_update_lower_bound(&workspace, %sCanon_Params.l);\n' % info_opt[C.PREFIX])
                f.write('  }\n')
            if info_can[C.P_ID_TO_CHANGES]['u']:
                f.write('  if (%sCanon_Outdated.u) {\n' % info_opt[C.PREFIX])
                f.write('    %scpg_canonicalize_u();\n' % info_opt[C.PREFIX])
                f.write('    osqp_update_upper_bound(&workspace, %sCanon_Params.u);\n' % info_opt[C.PREFIX])
                f.write('  }\n')

    elif info_opt[C.SOLVER_NAME] in ['SCS', 'ECOS']:

        for p_id, changes in info_can[C.P_ID_TO_CHANGES].items():
            if changes:
                f.write('  if (%sCanon_Outdated.%s) {\n' % (info_opt[C.PREFIX], p_id))
                f.write('    %scpg_canonicalize_%s();\n' % (info_opt[C.PREFIX], p_id))
                f.write('  }\n')

    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('  // Solve with OSQP\n')
        f.write('  osqp_solve(&workspace);\n')
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('  // Solve with SCS\n')
        f.write('  if (initialized == 0 || %sCanon_Outdated.A) {\n' % info_opt[C.PREFIX])
        f.write('    %sScs_Work = scs_init(&%sScs_D, &%sScs_K, &%sScs_Stgs);\n' %
                (info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('    initialized = 1;\n')
        f.write('  } else if (%sCanon_Outdated.b && %sCanon_Outdated.c) {\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('    scs_update(%sScs_Work, %sCanon_Params.b, %sCanon_Params.c);\n' %
                (info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  } else if (%sCanon_Outdated.b) {\n' % info_opt[C.PREFIX])
        f.write('    scs_update(%sScs_Work, %sCanon_Params.b, SCS_NULL);\n' %
                (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  } else if (%sCanon_Outdated.c) {\n' % info_opt[C.PREFIX])
        f.write('    scs_update(%sScs_Work, SCS_NULL, %sCanon_Params.c);\n' %
                (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('  }\n')
        f.write('  scs_solve(%sScs_Work, &%sScs_Sol, &%sScs_Info, (initialized && %sScs_Stgs.warm_start));\n' %
                (info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX]))
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        for p_id, size in info_can[C.P_ID_TO_SIZE].items():
            if size == 1:
                f.write('  %sCanon_Params_ECOS.%s = %sCanon_Params.%s;\n'
                        % (info_opt[C.PREFIX], p_id, info_opt[C.PREFIX], p_id))
            elif size > 1:
                f.write('  for (i=0; i<%d; i++){\n' % size)
                if p_id.isupper():
                    f.write('    %sCanon_Params_ECOS.%s->x[i] = %sCanon_Params.%s->x[i];\n'
                            % (info_opt[C.PREFIX], p_id, info_opt[C.PREFIX], p_id))
                else:
                    f.write('    %sCanon_Params_ECOS.%s[i] = %sCanon_Params.%s[i];\n'
                            % (info_opt[C.PREFIX], p_id, info_opt[C.PREFIX], p_id))
                f.write('  }\n')
        f.write('  // Initialize / update ECOS workspace and settings\n')
        write_ecos_setup_update(f, info_can[C.CONSTANTS], info_opt[C.PREFIX])
        for name in info_can[C.SETTINGS_NAMES_TO_TYPE].keys():
            f.write('  %secos_workspace->stgs->%s = %sCanon_Settings.%s;\n'
                    % (info_opt[C.PREFIX], name, info_opt[C.PREFIX], name))
        f.write('  // Solve with ECOS\n')
        f.write('  %secos_flag = ECOS_solve(%secos_workspace);\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))

    f.write('  // Retrieve results\n')
    if info_cg[C.RET_PRIM_FUNC_EXISTS]:
        f.write('  %scpg_retrieve_prim();\n' % info_opt[C.PREFIX])
    if info_cg[C.RET_DUAL_FUNC_EXISTS]:
        f.write('  %scpg_retrieve_dual();\n' % info_opt[C.PREFIX])
    f.write('  %scpg_retrieve_info();\n' % info_opt[C.PREFIX])

    f.write('  // Reset flags for outdated canonical parameters\n')
    for p_id in info_can[C.P_ID_TO_SIZE].keys():
        f.write('  %sCanon_Outdated.%s = 0;\n' % (info_opt[C.PREFIX], p_id))

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

    f.write('// Update solver settings\n')
    f.write('void %scpg_set_solver_default_settings(){\n' % info_opt[C.PREFIX])
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('  osqp_set_default_settings(&settings);\n')
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('  scs_set_default_settings(&%sScs_Stgs);\n' % info_opt[C.PREFIX])
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        for name, value in info_can[C.SETTINGS_NAMES_TO_DEFAULT].items():
            f.write('  %sCanon_Settings.%s = %s;\n' % (info_opt[C.PREFIX], name, value))
    f.write('}\n')
    for name, typ in info_can[C.SETTINGS_NAMES_TO_TYPE].items():
        f.write('\nvoid %scpg_set_solver_%s(%s %s_new){\n' % (info_opt[C.PREFIX], name, typ, name))
        if info_opt[C.SOLVER_NAME] == 'OSQP':
            f.write('  osqp_update_%s(&workspace, %s_new);\n' % (name, name))
        elif info_opt[C.SOLVER_NAME] == 'SCS':
            f.write('  %sScs_Stgs.%s = %s_new;\n' % (info_opt[C.PREFIX], name, name))
        elif info_opt[C.SOLVER_NAME] == 'ECOS':
            f.write('  %sCanon_Settings.%s = %s_new;\n' % (info_opt[C.PREFIX], name, name))
        f.write('}\n')


def write_solve_prot(f, info_opt, info_cg, info_usr, info_can):
    """
    Write function declarations to file
    """

    write_description(f, 'c', 'Function declarations')
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('#include "types.h"\n')
    elif info_opt[C.SOLVER_NAME] in ['SCS', 'ECOS']:
        f.write('#include "cpg_workspace.h"\n')

    f.write('\n// Update user-defined parameter values\n')
    for name, size in info_usr[C.P_NAME_TO_SIZE].items():
        if size == 1:
            f.write('extern void %scpg_update_%s(c_float val);\n' % (info_opt[C.PREFIX], name))
        else:
            f.write('extern void %scpg_update_%s(c_int idx, c_float val);\n' % (info_opt[C.PREFIX], name))

    f.write('\n// Map user-defined to canonical parameters\n')
    for p_id, changes in info_can[C.P_ID_TO_CHANGES].items():
        if changes:
            f.write('extern void %scpg_canonicalize_%s();\n' % (info_opt[C.PREFIX], p_id))

    if info_cg[C.RET_PRIM_FUNC_EXISTS]:
        f.write('\n// Retrieve primal solution in terms of user-defined variables\n')
        f.write('extern void %scpg_retrieve_prim();\n' % info_opt[C.PREFIX])

    if info_cg[C.RET_DUAL_FUNC_EXISTS]:
        f.write('\n// Retrieve dual solution in terms of user-defined constraints\n')
        f.write('extern void %scpg_retrieve_dual();\n' % info_opt[C.PREFIX])

    f.write('\n// Retrieve solver information\n')
    f.write('extern void %scpg_retrieve_info();\n' % info_opt[C.PREFIX])

    f.write('\n// Solve via canonicalization, canonical solve, retrieval\n')
    f.write('extern void %scpg_solve();\n' % info_opt[C.PREFIX])

    f.write('\n// Update solver settings\n')
    f.write('extern void %scpg_set_solver_default_settings();\n' % info_opt[C.PREFIX])
    for name, typ in info_can[C.SETTINGS_NAMES_TO_TYPE].items():
        f.write('extern void %scpg_set_solver_%s(%s %s_new);\n' % (info_opt[C.PREFIX], name, typ, name))


def write_example_def(f, info_opt, info_usr):
    """
    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 c_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 info_usr[C.P_WRITABLE].items():
        if np.isscalar(value):
            f.write('  %scpg_update_%s(%.20f);\n' % (info_opt[C.PREFIX], name, value))
        else:
            f.write('  %scpg_update_%s(0, %.20f);\n' % (info_opt[C.PREFIX], name, value[0]))

    f.write('\n  // Solve the problem instance\n')
    f.write('  %scpg_solve();\n\n' % info_opt[C.PREFIX])

    f.write('  // Print objective function value\n')
    f.write('  printf("obj = %%f\\n", %sCPG_Result.info->obj_val);\n\n' % info_opt[C.PREFIX])

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

    if info_opt[C.SOLVER_NAME] == 'OSQP':
        int_format_str = 'lld'
    else:
        int_format_str = 'd'

    for name, var in info_usr[C.V_NAME_TO_INIT].items():
        if np.isscalar(var):
            f.write('  printf("%s = %%f\\n", %sCPG_Result.prim->%s);\n' % (name, info_opt[C.PREFIX], name))
        else:
            f.write('  for(i=0; i<%d; i++) {\n' % var.size)
            f.write('    printf("%s[%%%s] = %%f\\n", i, %sCPG_Result.prim->%s[i]);\n'
                    % (name, int_format_str, info_opt[C.PREFIX], name))
            f.write('  }\n')

    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('\n  // Print dual solution\n')
    for name, var in info_usr[C.D_NAME_TO_INIT].items():
        if np.isscalar(var):
            f.write('  printf("%s = %%f\\n", %sCPG_Result.dual->%s);\n' % (name, info_opt[C.PREFIX], name))
        else:
            f.write('  for(i=0; i<%d; i++) {\n' % var.size)
            f.write('    printf("%s[%%%s] = %%f\\n", i, %sCPG_Result.dual->%s[i]);\n'
                    % (name, int_format_str, info_opt[C.PREFIX], name))
            f.write('  }\n')

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


def replace_cmake_data(cmake_data, info_opt):
    """
    Add C.PREFIX 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', info_opt[C.PREFIX]+'cpg_include')
    cmake_data = cmake_data.replace('cpg_head', info_opt[C.PREFIX] + 'cpg_head')
    return cmake_data.replace('cpg_src', info_opt[C.PREFIX] + 'cpg_src')


def write_canon_cmake(f, info_opt):
    """
    Pass sources to parent scope in {OSQP/ECOS}_code/CMakeLists.txt
    """

    if info_opt[C.SOLVER_NAME] == 'OSQP':
        f.write('\nset(solver_head "${osqp_headers}" PARENT_SCOPE)')
        f.write('\nset(solver_src "${osqp_src}" PARENT_SCOPE)')
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        f.write('\nset(solver_head')
        f.write('\n  ${${PROJECT_NAME}_HDR}')
        f.write('\n  ${DIRSRC}/private.h')
        f.write('\n  ${${PROJECT_NAME}_LDL_EXTERNAL_HDR}')
        f.write('\n  ${${PROJECT_NAME}_AMD_EXTERNAL_HDR})')
        f.write('\nset(solver_src')
        f.write('\n  ${${PROJECT_NAME}_SRC}')
        f.write('\n  ${DIRSRC}/private.c')
        f.write('\n  ${EXTERNAL}/qdldl/qdldl.c')
        f.write('\n  ${${PROJECT_NAME}_AMD_EXTERNAL_SRC})')
        f.write('\n\nset(solver_head "${solver_head}" PARENT_SCOPE)')
        f.write('\nset(solver_src "${solver_src}" PARENT_SCOPE)')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('\nset(solver_head "${ecos_headers}" PARENT_SCOPE)')
        f.write('\nset(solver_src "${ecos_sources}" PARENT_SCOPE)')


def write_module_def(f, info_opt, info_usr, info_can):
    """
    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')
    f.write('}\n\n')
    f.write('namespace py = pybind11;\n\n')
    f.write('static int i;\n\n')

    # cpp function that maps parameters to results
    f.write('%sCPG_Result_cpp_t %ssolve_cpp(struct %sCPG_Updated_cpp_t& CPG_Updated_cpp, '
            'struct %sCPG_Params_cpp_t& CPG_Params_cpp){\n\n'
            % (info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX]))

    f.write('    // Pass changed user-defined parameter values to the solver\n')
    for name, size in info_usr[C.P_NAME_TO_SIZE].items():
        f.write('    if (CPG_Updated_cpp.%s) {\n' % name)
        if size == 1:
            f.write('        %scpg_update_%s(CPG_Params_cpp.%s);\n' % (info_opt[C.PREFIX], name, name))
        else:
            f.write('        for(i=0; i<%d; i++) {\n' % size)
            f.write('            %scpg_update_%s(i, CPG_Params_cpp.%s[i]);\n' % (info_opt[C.PREFIX], name, name))
            f.write('        }\n')
        f.write('    }\n')

    # perform ASA procedure
    f.write('\n    // Solve\n')
    f.write('    std::clock_t ASA_start = std::clock();\n')
    f.write('    %scpg_solve();\n' % info_opt[C.PREFIX])
    f.write('    std::clock_t ASA_end = std::clock();\n\n')

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

    f.write('    %sCPG_Prim_cpp_t CPG_Prim_cpp {};\n' % info_opt[C.PREFIX])
    for name, var in info_usr[C.V_NAME_TO_INIT].items():
        if np.isscalar(var):
            f.write('    CPG_Prim_cpp.%s = %sCPG_Prim.%s;\n' % (name, info_opt[C.PREFIX], name))
        else:
            f.write('    for(i=0; i<%d; i++) {\n' % var.size)
            f.write('        CPG_Prim_cpp.%s[i] = %sCPG_Prim.%s[i];\n'
                    % (name, info_opt[C.PREFIX], name))
            f.write('    }\n')

    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('    %sCPG_Dual_cpp_t CPG_Dual_cpp {};\n' % info_opt[C.PREFIX])
        for name, var in info_usr[C.D_NAME_TO_INIT].items():
            if np.isscalar(var):
                f.write('    CPG_Dual_cpp.%s = %sCPG_Dual.%s;\n' % (name, info_opt[C.PREFIX], name))
            else:
                f.write('    for(i=0; i<%d; i++) {\n' % var.size)
                f.write('        CPG_Dual_cpp.%s[i] = %sCPG_Dual.%s[i];\n' % (name, info_opt[C.PREFIX], name))
                f.write('    }\n')

    f.write('    %sCPG_Info_cpp_t CPG_Info_cpp {};\n' % info_opt[C.PREFIX])
    for field in ['obj_val', 'iter', 'status', 'pri_res', 'dua_res']:
        f.write('    CPG_Info_cpp.%s = %sCPG_Info.%s;\n' % (field, info_opt[C.PREFIX], field))
    f.write('    CPG_Info_cpp.time = 1.0*(ASA_end-ASA_start) / CLOCKS_PER_SEC;\n')

    f.write('    %sCPG_Result_cpp_t CPG_Result_cpp {};\n' % info_opt[C.PREFIX])
    f.write('    CPG_Result_cpp.prim = CPG_Prim_cpp;\n')
    if len(info_usr[C.D_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')

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

    f.write('    py::class_<%sCPG_Params_cpp_t>(m, "%scpg_params")\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    f.write('            .def(py::init<>())\n')
    for name in info_usr[C.P_NAME_TO_SIZE].keys():
        f.write('            .def_readwrite("%s", &%sCPG_Params_cpp_t::%s)\n' % (name, info_opt[C.PREFIX], name))
    f.write('            ;\n\n')

    f.write('    py::class_<%sCPG_Updated_cpp_t>(m, "%scpg_updated")\n'
            % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    f.write('            .def(py::init<>())\n')
    for name in info_usr[C.P_NAME_TO_SIZE].keys():
        f.write('            .def_readwrite("%s", &%sCPG_Updated_cpp_t::%s)\n' % (name, info_opt[C.PREFIX], name))
    f.write('            ;\n\n')

    f.write('    py::class_<%sCPG_Prim_cpp_t>(m, "%scpg_prim")\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    f.write('            .def(py::init<>())\n')
    for name in info_usr[C.V_NAME_TO_INIT].keys():
        f.write('            .def_readwrite("%s", &%sCPG_Prim_cpp_t::%s)\n' % (name, info_opt[C.PREFIX], name))
    f.write('            ;\n\n')

    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('    py::class_<%sCPG_Dual_cpp_t>(m, "%scpg_dual")\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
        f.write('            .def(py::init<>())\n')
        for name in info_usr[C.D_NAME_TO_INIT].keys():
            f.write('            .def_readwrite("%s", &%sCPG_Dual_cpp_t::%s)\n' % (name, info_opt[C.PREFIX], name))
        f.write('            ;\n\n')

    f.write('    py::class_<%sCPG_Info_cpp_t>(m, "%scpg_info")\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    f.write('            .def(py::init<>())\n')
    f.write('            .def_readwrite("obj_val", &%sCPG_Info_cpp_t::obj_val)\n' % info_opt[C.PREFIX])
    f.write('            .def_readwrite("iter", &%sCPG_Info_cpp_t::iter)\n' % info_opt[C.PREFIX])
    f.write('            .def_readwrite("status", &%sCPG_Info_cpp_t::status)\n' % info_opt[C.PREFIX])
    f.write('            .def_readwrite("pri_res", &%sCPG_Info_cpp_t::pri_res)\n' % info_opt[C.PREFIX])
    f.write('            .def_readwrite("dua_res", &%sCPG_Info_cpp_t::dua_res)\n' % info_opt[C.PREFIX])
    f.write('            .def_readwrite("time", &%sCPG_Info_cpp_t::time)\n' % info_opt[C.PREFIX])
    f.write('            ;\n\n')

    f.write('    py::class_<%sCPG_Result_cpp_t>(m, "%scpg_result")\n' % (info_opt[C.PREFIX], info_opt[C.PREFIX]))
    f.write('            .def(py::init<>())\n')
    f.write('            .def_readwrite("cpg_prim", &%sCPG_Result_cpp_t::prim)\n' % info_opt[C.PREFIX])
    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('            .def_readwrite("cpg_dual", &%sCPG_Result_cpp_t::dual)\n' % info_opt[C.PREFIX])
    f.write('            .def_readwrite("cpg_info", &%sCPG_Result_cpp_t::info)\n' % info_opt[C.PREFIX])
    f.write('            ;\n\n')

    f.write('    m.def("solve", &%ssolve_cpp);\n\n' % info_opt[C.PREFIX])

    f.write('    m.def("set_solver_default_settings", &%scpg_set_solver_default_settings);\n' % info_opt[C.PREFIX])
    for name in info_can[C.SETTINGS_NAMES_TO_TYPE].keys():
        f.write('    m.def("set_solver_%s", &%scpg_set_solver_%s);\n' % (name, info_opt[C.PREFIX], name))

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


def write_module_prot(f, info_opt, info_usr):
    """
    Write c++ file for pbind11 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('struct %sCPG_Params_cpp_t {\n' % info_opt[C.PREFIX])
    for name, size in info_usr[C.P_NAME_TO_SIZE].items():
        if size == 1:
            f.write('    double %s;\n' % name)
        else:
            f.write('    std::array<double, %d> %s;\n' % (size, name))
    f.write('};\n\n')

    # cpp struct containing update flags for user-defined parameters
    f.write('// Flags for updated user-defined parameters\n')
    f.write('struct %sCPG_Updated_cpp_t {\n' % info_opt[C.PREFIX])
    for name in info_usr[C.P_NAME_TO_SIZE].keys():
        f.write('    bool %s;\n' % name)
    f.write('};\n\n')

    # cpp struct containing primal variables
    f.write('// Primal solution\n')
    f.write('struct %sCPG_Prim_cpp_t {\n' % info_opt[C.PREFIX])
    for name, var in info_usr[C.V_NAME_TO_INIT].items():
        if np.isscalar(var):
            f.write('    double %s;\n' % name)
        else:
            f.write('    std::array<double, %d> %s;\n' % (var.size, name))
    f.write('};\n\n')

    # cpp struct containing dual variables
    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('// Dual solution\n')
        f.write('struct %sCPG_Dual_cpp_t {\n' % info_opt[C.PREFIX])
        for name, var in info_usr[C.D_NAME_TO_INIT].items():
            if np.isscalar(var):
                f.write('    double %s;\n' % name)
            else:
                f.write('    std::array<double, %d> %s;\n' % (var.size, name))
        f.write('};\n\n')

    # cpp struct containing info on results
    f.write('// Solver information\n')
    f.write('struct %sCPG_Info_cpp_t {\n' % info_opt[C.PREFIX])
    f.write('    double obj_val;\n')
    f.write('    int iter;\n')
    if info_opt[C.SOLVER_NAME] in ['OSQP', 'SCS']:
        f.write('    char* status;\n')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('    int 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('struct %sCPG_Result_cpp_t {\n' % info_opt[C.PREFIX])
    f.write('    %sCPG_Prim_cpp_t prim;\n' % info_opt[C.PREFIX])
    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        f.write('    %sCPG_Dual_cpp_t dual;\n' % info_opt[C.PREFIX])
    f.write('    %sCPG_Info_cpp_t info;\n' % info_opt[C.PREFIX])
    f.write('};\n\n')

    # cpp function that maps parameters to results
    f.write('// Main solve function\n')
    f.write('%sCPG_Result_cpp_t %ssolve_cpp(struct %sCPG_Updated_cpp_t& CPG_Updated_cpp, '
            'struct %sCPG_Params_cpp_t& CPG_Params_cpp);\n'
            % (info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX], info_opt[C.PREFIX]))


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, info_opt, info_usr):
    """
    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' % info_opt[C.CODE_DIR].replace('/', '.').replace('\\', '.'))

    if info_opt[C.SOLVER_NAME] == 'ECOS':
        indent = ' ' * 24
        f.write('status_int_to_string = {0: "Optimal solution found", \n' +
                indent + '1: "Certificate of primal infeasibility found", \n' +
                indent + '2: "Certificate of dual infeasibility found", \n' +
                indent + '10: "Optimal solution found subject to reduced tolerances", \n' +
                indent + '11: "Certificate of primal infeasibility found subject to reduced tolerances", \n' +
                indent + '12: "Certificate of dual infeasibility found subject to reduced tolerances", \n' +
                indent + '-1: "Maximum number of iterations reached", \n' +
                indent + '-2: "Numerical problems (unreliable search direction)", \n' +
                indent + '-3: "Numerical problems (slacks or multipliers outside cone)", \n' +
                indent + '-4: "Interrupted by signal or CTRL-C", \n' +
                indent + '-7: "Unknown problem in solver", \n' +
                indent + '-99: "Unknown problem before solving"}\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('    upd = cpg_module.%scpg_updated()\n' % info_opt[C.PREFIX])
    f.write('    if updated_params is None:\n')
    p_list_string = ''
    for name in info_usr[C.P_NAME_TO_SIZE].keys():
        p_list_string += '"%s", ' % name
    f.write('        updated_params = [%s]\n' % p_list_string[:-2])
    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("%s is not a parameter." % p))\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')
    if info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('        if key == "max_iters":\n')
        f.write('            key = "maxit"\n')
    f.write('        try:\n')
    f.write('            eval(\'cpg_module.set_solver_%s(value)\' % key)\n')
    f.write('        except AttributeError:\n')
    f.write('            raise(AttributeError(\'Solver setting "%s" not available.\' % key))\n\n')

    f.write('    # set parameter values\n')
    f.write('    par = cpg_module.%scpg_params()\n' % info_opt[C.PREFIX])
    for name, size in info_usr[C.P_NAME_TO_SIZE].items():
        if name in info_usr[C.P_NAME_TO_SPARSITY].keys():
            f.write('    n = prob.param_dict[\'%s\'].shape[0]\n' % name)
            if info_usr[C.P_NAME_TO_SPARSITY_TYPE][name] == 'diag':
                f.write('    %s_coordinates = np.arange(0, n**2, n+1)\n' % name)
            else:
                f.write('    %s_coordinates = np.unique([coord[0]+coord[1]*n for coord in '
                        'prob.param_dict[\'%s\'].attributes[\'sparsity\']])\n' % (name, name))
            if size == 1:
                f.write('    par.%s = prob.param_dict[\'%s\'].value[coordinates]\n' % (name, name))
            else:
                f.write('    %s_value = []\n' % name)
                f.write('    %s_flat = prob.param_dict[\'%s\'].value.flatten(order=\'F\')\n' % (name, name))
                f.write('    for coord in %s_coordinates:\n' % name)
                f.write('        %s_value.append(%s_flat[coord])\n' % (name, name))
                f.write('        %s_flat[coord] = 0\n' % name)
                f.write('    if np.sum(np.abs(%s_flat)) > 0:\n' % name)
                f.write('        warnings.warn(\'Ignoring nonzero value outside of sparsity pattern for '
                        'parameter %s!\')\n' % name)
                f.write('    par.%s = list(%s_value)\n' % (name, name))
        else:
            if size == 1:
                f.write('    par.%s = prob.param_dict[\'%s\'].value\n' % (name, name))
            else:
                f.write('    par.%s = list(prob.param_dict[\'%s\'].value.flatten(order=\'F\'))\n' % (name, name))

    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 info_usr[C.V_NAME_TO_SHAPE].items():
        if len(shape) == 2:
            f.write('    prob.var_dict[\'%s\'].value = np.array(res.cpg_prim.%s).reshape((%d, %d), order=\'F\')\n' %
                    (name, name, shape[0], shape[1]))
        else:
            f.write('    prob.var_dict[\'%s\'].value = np.array(res.cpg_prim.%s)\n' % (name, name))
    for i, (name, shape) in enumerate(info_usr[C.D_NAME_TO_SHAPE].items()):
        if len(shape) == 2:
            f.write('    prob.constraints[%d].save_dual_value('
                    'np.array(res.cpg_dual.%s).reshape((%d, %d), order=\'F\'))\n' % (i, name, shape[0], shape[1]))
        else:
            f.write('    prob.constraints[%d].save_dual_value(np.array(res.cpg_dual.%s))\n' % (i, name))

    f.write('\n    # store additional solver information in problem object\n')
    if info_opt[C.SOLVER_NAME] in ['OSQP', 'SCS']:
        f.write('    prob._status = res.cpg_info.status\n')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        f.write('    prob._status = status_int_to_string[res.cpg_info.status]\n')
    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('    prob._solver_stats = SolverStats(results_dict, \'%s\')\n\n' % info_opt[C.SOLVER_NAME])

    f.write('    return prob.value\n')


def replace_html_data(text, info_opt, info_usr):
    """
    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(info_usr[C.P_NAME_TO_SHAPE], info_usr[C.P_NAME_TO_SIZE]))

    # primal variable summary
    text = text.replace('$PRIMALS', write_problem_summary(info_usr[C.V_NAME_TO_SHAPE], info_usr[C.V_NAME_TO_SIZE]))

    # dual variable summary
    text = text.replace('$DUALS', write_problem_summary(info_usr[C.D_NAME_TO_SHAPE], info_usr[C.D_NAME_TO_SIZE]))

    # code_dir
    text = text.replace('$CODEDIR', info_opt[C.CODE_DIR])
    text = text.replace('$CDPYTHON', info_opt[C.CODE_DIR].replace('/', '.').replace('\\', '.'))

    # solver name and docu
    text = text.replace('$CPGSOLVERNAME', info_opt[C.SOLVER_NAME])
    if info_opt[C.SOLVER_NAME] == 'OSQP':
        text = text.replace('$CPGSOLVERDOCUURL', 'https://osqp.org/docs/codegen/python.html')
    elif info_opt[C.SOLVER_NAME] == 'SCS':
        text = text.replace('$CPGSOLVERDOCUURL', 'https://www.cvxgrp.org/scs/api/c.html')
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        text = text.replace('$CPGSOLVERDOCUURL', 'https://github.com/embotech/ecos/wiki/Usage-from-C')

    # CMake prefix
    text = text.replace('$CPGCMAKELISTS', info_opt[C.PREFIX]+'cpg')

    # type definition of CPG_Prim_t
    CPGPRIMTYPEDEF = '\n// Struct type with primal solution\n'
    CPGPRIMTYPEDEF += 'typedef struct {\n'
    for name, var in info_usr[C.V_NAME_TO_INIT].items():
        if np.isscalar(var):
            s = ''
        else:
            s = '*'
        CPGPRIMTYPEDEF += ('  c_float    %s   // Your variable %s\n' % ((s + name + ';').ljust(9), name))
    CPGPRIMTYPEDEF += '} CPG_Prim_t;\n'
    text = text.replace('$CPGPRIMTYPEDEF', CPGPRIMTYPEDEF)

    # type definition of CPG_Dual_t
    if len(info_usr[C.D_NAME_TO_INIT]) > 0:
        CPGDUALTYPEDEF = '\n// Struct type with dual solution\n'
        CPGDUALTYPEDEF += 'typedef struct {\n'
        for name, var in info_usr[C.D_NAME_TO_INIT].items():
            if np.isscalar(var):
                s = ''
            else:
                s = '*'
            CPGDUALTYPEDEF += ('  c_float    %s   // Your dual variable for constraint %s\n'
                               % ((s + name + ';').ljust(9), name))
        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 += '  c_float    obj_val;    // Objective function value\n'
    CPGINFOTYPEDEF += '  c_int      iter;       // Number of iterations\n'
    if info_opt[C.SOLVER_NAME] in ['OSQP', 'SCS']:
        CPGINFOTYPEDEF += '  char       *status;    // Solver status\n'
    elif info_opt[C.SOLVER_NAME] == 'ECOS':
        CPGINFOTYPEDEF += '  c_int      status;     // Solver status\n'
    CPGINFOTYPEDEF += '  c_float    pri_res;    // Primal residual\n'
    CPGINFOTYPEDEF += '  c_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(info_usr[C.D_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 info_usr[C.P_NAME_TO_SIZE].items():
        if size == 1:
            CPGUPDATEDECLARATIONS += 'void %scpg_update_%s(c_float value);\n' % (info_opt[C.PREFIX], name)
        else:
            CPGUPDATEDECLARATIONS += 'void %scpg_update_%s(c_int idx, c_float value);\n' % (info_opt[C.PREFIX], name)
    text = text.replace('$CPGUPDATEDECLARATIONS', CPGUPDATEDECLARATIONS)

    # solve declarations
    CPGSOLVEDECLARATIONS = '\n// Solve via canonicalization, canonical solve, retrieval\n'
    CPGSOLVEDECLARATIONS += 'void %scpg_solve();\n' % info_opt[C.PREFIX]
    text = text.replace('$CPGSOLVEDECLARATIONS', CPGSOLVEDECLARATIONS)

    # settings declarations
    CPGSETTINGSDECLARATIONS = '\n// Update solver settings\n'
    CPGSETTINGSDECLARATIONS += 'void %scpg_set_solver_default_settings();\n' % info_opt[C.PREFIX]
    CPGSETTINGSDECLARATIONS += 'void %scpg_set_solver_&lt;setting_name&gt;' \
                               '(&lt;setting_type&gt; &lt;setting_name&gt;_new);\n' % info_opt[C.PREFIX]
    CPGSETTINGSDECLARATIONS += '...\n'

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