"""Handling of parameters."""

# Authors: Mainak Jas <mjas@mgh.harvard.edu>
#          Sam Neymotin <samnemo@gmail.com>

import json
import fnmatch
import os.path as op
from pathlib import Path
from copy import deepcopy

from .params_default import get_params_default
from .externals.mne import _validate_type


# return number of evoked inputs (proximal, distal)
# using dictionary d (or if d is a string, first load the dictionary from
# filename d)
def _count_evoked_inputs(d):
    nprox = ndist = 0
    for k, _ in d.items():
        if k.startswith('t_'):
            if k.count('evprox') > 0:
                nprox += 1
            elif k.count('evdist') > 0:
                ndist += 1
    return nprox, ndist


def _read_json(param_data):
    """Read param values from a .json file.
    Parameters
    ----------
    param_data : str
        The data read in from the param file

    Returns
    -------
    params_input : dict
        Dictionary of parameters
    """
    return json.loads(param_data)


def _read_legacy_params(param_data):
    """Read param values from a .param file (legacy).
    Parameters
    ----------
    param_data : str
        The data read in from the param file

    Returns
    -------
    params_input : dict
        Dictionary of parameters
    """
    params_input = dict()
    for line in param_data.splitlines():
        split_line = line.lstrip().split(':')
        key, value = [field.strip() for field in split_line]
        try:
            if '.' in value or 'e' in value:
                params_input[key] = float(value)
            else:
                params_input[key] = int(value)
        except ValueError:
            params_input[key] = str(value)

    return params_input


def read_params(params_fname, file_contents=None):
    """Read param values from a file (.json or .param).

    Parameters
    ----------
    params_fname : str
        Full path to the file (.param)
    file_contents : str | None
        If file_contents are provided as a string,
        it is parsed into a dictionary.

    Returns
    -------
    params : an instance of Params
        Params containing parameter values from file
    """

    split_fname = op.splitext(params_fname)
    ext = split_fname[1]

    if ext not in ['.json', '.param']:
        raise ValueError('Unrecognized extension, expected one of' +
                         ' .json, .param. Got %s' % ext)

    if file_contents is None:
        with open(params_fname, 'r') as fp:
            file_contents = fp.read()

    read_func = {'.json': _read_json, '.param': _read_legacy_params}
    params_dict = read_func[ext](file_contents)

    if len(params_dict) == 0:
        raise ValueError("Failed to read parameters from file: %s" %
                         op.normpath(params_fname))

    params = Params(params_dict)

    return params


def _long_name(short_name):
    long_name = dict(L2Basket='L2_basket', L5Basket='L5_basket',
                     L2Pyr='L2_pyramidal', L5Pyr='L5_pyramidal')
    if short_name in long_name:
        return long_name[short_name]
    return short_name


def _short_name(short_name):
    long_name = dict(L2_basket='L2Basket', L5_basket='L5Basket',
                     L2_pyramidal='L2Pyr', L5_pyramidal='L5Pyr')
    if short_name in long_name:
        return long_name[short_name]
    return short_name


def _extract_bias_specs_from_hnn_params(params, cellname_list):
    """Create 'bias specification' dicts from saved parameters"""
    bias_specs = {'tonic': {}}  # currently only 'tonic' biases known
    for cellname in cellname_list:
        short_name = _short_name(cellname)
        is_tonic_present = [f'Itonic_{p}_{short_name}_soma' in
                            params for p in ['A', 't0', 'T']]
        if any(is_tonic_present):
            if not all(is_tonic_present):
                raise ValueError(
                    f'Tonic input must have the amplitude, '
                    f'start time and end time specified. One '
                    f'or more parameter may be missing for '
                    f'cell type {cellname}')
            bias_specs['tonic'][cellname] = {
                'amplitude': params[f'Itonic_A_{short_name}_soma'],
                't0': params[f'Itonic_t0_{short_name}_soma'],
                'tstop': params[f'Itonic_T_{short_name}_soma']
            }
    return bias_specs


def _extract_drive_specs_from_hnn_params(
        params, cellname_list, legacy_mode=False):
    """Create 'drive specification' dicts from saved parameters"""
    # convert legacy params-dict to legacy "feeds" dicts
    p_common, p_unique = create_pext(params, params['tstop'])

    # Using 'feed' for legacy compatibility, 'drives' for new API
    drive_specs = dict()
    for ic, par in enumerate(p_common):
        if (not legacy_mode) and par['tstop'] < par['t0']:
            continue
        feed_name = f'bursty{ic + 1}'
        drive = dict()
        drive['type'] = 'bursty'
        drive['cell_specific'] = False
        drive['dynamics'] = {'tstart': par['t0'],
                             'tstart_std': par['t0_stdev'],
                             'tstop': par['tstop'],
                             'burst_rate': par['f_input'],
                             'burst_std': par['stdev'],
                             'numspikes': par['events_per_cycle'],
                             'n_drive_cells': par['n_drive_cells'],
                             'spike_isi': 10}  # not exposed in params-files
        drive['location'] = par['loc']
        drive['space_constant'] = par['lamtha']
        drive['event_seed'] = par['prng_seedcore']
        drive['weights_ampa'] = dict()
        drive['weights_nmda'] = dict()
        drive['synaptic_delays'] = dict()

        for cellname in cellname_list:
            cname_ampa = _short_name(cellname) + '_ampa'
            cname_nmda = _short_name(cellname) + '_nmda'
            if cname_ampa in par:
                ampa_weight = par[cname_ampa][0]
                ampa_delay = par[cname_ampa][1]
                drive['weights_ampa'][cellname] = ampa_weight

                # NB synaptic delay same for NMDA, read only for AMPA
                drive['synaptic_delays'][cellname] = ampa_delay

            if cname_nmda in par:
                nmda_weight = par[cname_nmda][0]
                drive['weights_nmda'][cellname] = nmda_weight

        drive_specs[feed_name] = drive

    for feed_name, par in p_unique.items():
        drive = dict()
        drive['cell_specific'] = True
        drive['weights_ampa'] = dict()
        drive['weights_nmda'] = dict()
        drive['synaptic_delays'] = dict()

        if (feed_name.startswith('evprox') or
                feed_name.startswith('evdist')):
            drive['type'] = 'evoked'
            if feed_name.startswith('evprox'):
                drive['location'] = 'proximal'
            else:
                drive['location'] = 'distal'

            cell_keys_present = [key for key in par if
                                 key in cellname_list]
            sigma = par[cell_keys_present[0]][3]  # IID for all cells!

            n_drive_cells = 'n_cells'
            if par['sync_evinput']:
                n_drive_cells = 1
                drive['cell_specific'] = False

            drive['dynamics'] = {'mu': par['t0'],
                                 'sigma': sigma,
                                 'numspikes': par['numspikes'],
                                 'n_drive_cells': n_drive_cells}
            drive['space_constant'] = par['lamtha']
            drive['event_seed'] = par['prng_seedcore']
            # XXX Force random states to be the same as HNN-gui for the default
            # parameter set after increasing the number of bursty drive
            # gids from 2 to 20
            if legacy_mode:
                drive['event_seed'] -= 18
            for cellname in cellname_list:
                if cellname in par:
                    ampa_weight = par[cellname][0]
                    nmda_weight = par[cellname][1]
                    synaptic_delays = par[cellname][2]
                    drive['weights_ampa'][cellname] = ampa_weight
                    drive['weights_nmda'][cellname] = nmda_weight
                    drive['synaptic_delays'][cellname] = synaptic_delays

        # Skip drive if not in legacy mode
        elif feed_name.startswith('extgauss'):
            if (not legacy_mode) and par[
                    'L2_basket'][3] > params['tstop']:
                continue
            drive['type'] = 'gaussian'
            drive['location'] = par['loc']

            drive['dynamics'] = {'mu': par['L2_basket'][3],  # NB IID
                                 'sigma': par['L2_basket'][4],
                                 'numspikes': 50,  # NB hard-coded in GUI!
                                 'sync_within_trial': False}
            drive['space_constant'] = par['lamtha']
            drive['event_seed'] = par['prng_seedcore']

            for cellname in cellname_list:
                if cellname in par:
                    ampa_weight = par[cellname][0]
                    synaptic_delays = par[cellname][3]
                    drive['weights_ampa'][cellname] = ampa_weight
                    drive['synaptic_delays'][cellname] = synaptic_delays

            drive['weights_nmda'] = dict()  # no NMDA weights for Gaussians
        elif feed_name.startswith('extpois'):
            if (not legacy_mode) and par['t_interval'][1] < par[
                    't_interval'][0]:
                continue
            drive['type'] = 'poisson'
            drive['location'] = par['loc']
            drive['space_constant'] = par['lamtha']
            drive['event_seed'] = par['prng_seedcore']

            rate_params = dict()
            for cellname in cellname_list:
                if cellname in par:
                    rate_params[cellname] = par[cellname][3]
                    # XXX correct for non-positive poisson rate constant that
                    # is specified in null poisson drives of legacy
                    # param files
                    if not rate_params[cellname] > 0:
                        rate_params[cellname] = 1
                    ampa_weight = par[cellname][0]
                    nmda_weight = par[cellname][1]
                    synaptic_delays = par[cellname][2]
                    drive['weights_ampa'][cellname] = ampa_weight
                    drive['weights_nmda'][cellname] = nmda_weight
                    drive['synaptic_delays'][cellname] = synaptic_delays

            # do NOT allow negative times sometimes used in param-files
            drive['dynamics'] = {'tstart': max(0, par['t_interval'][0]),
                                 'tstop': max(0, par['t_interval'][1]),
                                 'rate_constant': rate_params}

        drive_specs[feed_name] = drive
    return drive_specs


class Params(dict):
    """Params object.

    Parameters
    ----------
    params_input : dict | None
        Dictionary of parameters. If None, use default parameters.
    """

    def __init__(self, params_input=None):

        if params_input is None:
            params_input = dict()

        if isinstance(params_input, dict):
            nprox, ndist = _count_evoked_inputs(params_input)
            # create default params templated from params_input
            params_default = get_params_default(nprox, ndist)

            for key in params_default.keys():
                if key in params_input:
                    self[key] = params_input[key]
                else:
                    self[key] = params_default[key]
        else:
            raise ValueError('params_input must be dict or None. Got %s'
                             % type(params_input))

    def __repr__(self):
        """Display the params nicely."""
        return json.dumps(self, sort_keys=True, indent=4)

    def __getitem__(self, key):
        """Return a subset of parameters."""
        keys = self.keys()
        if key in keys:
            return dict.__getitem__(self, key)
        else:
            matches = fnmatch.filter(keys, key)
            if len(matches) == 0:
                return dict.__getitem__(self, key)
            params = self.copy()
            for key in keys:
                if key not in matches:
                    params.pop(key)
            return params

    def __setitem__(self, key, value):
        """Set the value for a subset of parameters."""
        keys = self.keys()
        if key in keys:
            return dict.__setitem__(self, key, value)
        else:
            matches = fnmatch.filter(keys, key)
            if len(matches) == 0:
                return dict.__setitem__(self, key, value)
            for key in keys:
                if key in matches:
                    self.update({key: value})

    def copy(self):
        return deepcopy(self)

    def write(self, fname):
        """Write param values to a file.

        Parameters
        ----------
        fname : str
            Full path to the output file (.json)
        """
        with open(fname, 'w') as fp:
            json.dump(self, fp)


def _validate_feed(p_ext_d, tstop):
    """Validate external inputs that are fed to all
    cells uniformly (i.e., rather than individually).
    For now, this only includes rhythmic inputs.

    Parameters
    ----------
    p_ext_d : dict
        The parameter set to validate and append to p_ext.
    tstop : float
        Stop time of the simulation.

    Returns
    -------
    p_ext : list
        Cumulative list of dicts with newly appended ExtFeed.
    """

    # # reset tstop if the specified tstop exceeds the
    # # simulation runtime

    if p_ext_d['tstop'] > tstop:
        p_ext_d['tstop'] = tstop

    # if stdev is zero, increase synaptic weights 5 fold to make
    # single input equivalent to 5 simultaneous input to prevent spiking
    # <<---- SN: WHAT IS THIS RULE!?!?!?
    if not p_ext_d['stdev']:
        for key in p_ext_d.keys():
            if key.endswith('Pyr'):
                p_ext_d[key] = (p_ext_d[key][0] * 5., p_ext_d[key][1])
            elif key.endswith('Basket'):
                p_ext_d[key] = (p_ext_d[key][0] * 5., p_ext_d[key][1])

    # if L5 delay is -1, use same delays as L2 unless L2 delay is 0.1 in
    # which case use 1. <<---- SN: WHAT IS THIS RULE!?!?!?
    if p_ext_d['L5Pyr_ampa'][1] == -1:
        for key in p_ext_d.keys():
            if key.startswith('L5'):
                if p_ext_d['L2Pyr'][1] != 0.1:
                    p_ext_d[key] = (p_ext_d[key][0], p_ext_d['L2Pyr'][1])
                else:
                    p_ext_d[key] = (p_ext_d[key][0], 1.)

    return p_ext_d


def check_evoked_synkeys(p, nprox, ndist):
    # make sure ampa,nmda gbar values are in the param dict for evoked
    # inputs(for backwards compatibility)
    # evoked distal target cell types
    lctprox = ['L2Pyr', 'L5Pyr', 'L2Basket', 'L5Basket']
    # evoked proximal target cell types
    lctdist = ['L2Pyr', 'L5Pyr', 'L2Basket']
    lsy = ['ampa', 'nmda']  # synapse types used in evoked inputs
    for nev, pref, lct in zip([nprox, ndist], ['evprox_', 'evdist_'],
                              [lctprox, lctdist]):
        for i in range(nev):
            skey = pref + str(i + 1)
            for sy in lsy:
                for ct in lct:
                    k = 'gbar_' + skey + '_' + ct + '_' + sy
                    # if the synapse-specific gbar not present, use the
                    # existing weight for both ampa,nmda
                    if k not in p:
                        p[k] = p['gbar_' + skey + '_' + ct]

#


def check_pois_synkeys(p):
    # make sure ampa,nmda gbar values are in the param dict for Poisson inputs
    # (for backwards compatibility)
    lct = ['L2Pyr', 'L5Pyr', 'L2Basket', 'L5Basket']  # target cell types
    lsy = ['ampa', 'nmda']  # synapse types used in Poisson inputs
    for ct in lct:
        for sy in lsy:
            k = ct + '_Pois_A_weight_' + sy
            # if the synapse-specific weight not present, set it to 0 in p
            if k not in p:
                p[k] = 0.0

# creates the external feed params based on individual simulation params p


def create_pext(p, tstop):
    """Indexable Python list of param dicts for parallel.

    Turn off individual feeds by commenting out relevant line here.
    always valid, no matter the length.

    Parameters
    ----------
    p : dict
        The parameters returned by ExpParams(f_psim).return_pdict()
    """
    p_common = list()

    # p_unique is a dict of input param types that end up going to each cell
    # uniquely
    p_unique = dict()

    # default params for common proximal inputs
    feed_prox = {
        'f_input': p['f_input_prox'],
        't0': p['t0_input_prox'],
        'tstop': p['tstop_input_prox'],
        'stdev': p['f_stdev_prox'],
        'L2Pyr_ampa': (p['input_prox_A_weight_L2Pyr_ampa'],
                       p['input_prox_A_delay_L2']),
        'L2Pyr_nmda': (p['input_prox_A_weight_L2Pyr_nmda'],
                       p['input_prox_A_delay_L2']),
        'L5Pyr_ampa': (p['input_prox_A_weight_L5Pyr_ampa'],
                       p['input_prox_A_delay_L5']),
        'L5Pyr_nmda': (p['input_prox_A_weight_L5Pyr_nmda'],
                       p['input_prox_A_delay_L5']),
        'L2Basket_ampa': (p['input_prox_A_weight_L2Basket_ampa'],
                          p['input_prox_A_delay_L2']),
        'L2Basket_nmda': (p['input_prox_A_weight_L2Basket_nmda'],
                          p['input_prox_A_delay_L2']),
        'L5Basket_ampa': (p['input_prox_A_weight_L5Basket_ampa'],
                          p['input_prox_A_delay_L5']),
        'L5Basket_nmda': (p['input_prox_A_weight_L5Basket_nmda'],
                          p['input_prox_A_delay_L5']),
        'events_per_cycle': p['events_per_cycle_prox'],
        'prng_seedcore': int(p['prng_seedcore_input_prox']),
        'lamtha': 100.,
        'loc': 'proximal',
        'n_drive_cells': p['repeats_prox'],
        't0_stdev': p['t0_input_stdev_prox'],
        'threshold': p['threshold']
    }

    # ensures time interval makes sense
    p_common.append(_validate_feed(feed_prox, tstop))

    # default params for common distal inputs
    feed_dist = {
        'f_input': p['f_input_dist'],
        't0': p['t0_input_dist'],
        'tstop': p['tstop_input_dist'],
        'stdev': p['f_stdev_dist'],
        'L2Pyr_ampa': (p['input_dist_A_weight_L2Pyr_ampa'],
                       p['input_dist_A_delay_L2']),
        'L2Pyr_nmda': (p['input_dist_A_weight_L2Pyr_nmda'],
                       p['input_dist_A_delay_L2']),
        'L5Pyr_ampa': (p['input_dist_A_weight_L5Pyr_ampa'],
                       p['input_dist_A_delay_L5']),
        'L5Pyr_nmda': (p['input_dist_A_weight_L5Pyr_nmda'],
                       p['input_dist_A_delay_L5']),
        'L2Basket_ampa': (p['input_dist_A_weight_L2Basket_ampa'],
                          p['input_dist_A_delay_L2']),
        'L2Basket_nmda': (p['input_dist_A_weight_L2Basket_nmda'],
                          p['input_dist_A_delay_L2']),
        'events_per_cycle': p['events_per_cycle_dist'],
        'prng_seedcore': int(p['prng_seedcore_input_dist']),
        'lamtha': 100.,
        'loc': 'distal',
        'n_drive_cells': p['repeats_dist'],
        't0_stdev': p['t0_input_stdev_dist'],
        'threshold': p['threshold']
    }

    p_common.append(_validate_feed(feed_dist, tstop))

    nprox, ndist = _count_evoked_inputs(p)
    # print('nprox,ndist evoked inputs:', nprox, ndist)

    # NEW: make sure all evoked synaptic weights present
    # (for backwards compatibility)
    # could cause differences between output of param files
    # since some nmda weights should be 0 while others > 0

    # XXX dangerzone: params are modified in-place, values are imputed if
    # deemed missing (e.g. if 'gbar_evprox_1_L2Pyr_nmda' is not defined, the
    # code adds it to the p-dict with value: p['gbar_evprox_1_L2Pyr'])
    check_evoked_synkeys(p, nprox, ndist)

    # Create proximal evoked response parameters
    # f_input needs to be defined as 0
    for i in range(nprox):
        skey = 'evprox_' + str(i + 1)
        p_unique['evprox' + str(i + 1)] = {
            't0': p['t_' + skey],
            'L2_pyramidal': (p['gbar_' + skey + '_L2Pyr_ampa'],
                             p['gbar_' + skey + '_L2Pyr_nmda'],
                             0.1, p['sigma_t_' + skey]),
            'L2_basket': (p['gbar_' + skey + '_L2Basket_ampa'],
                          p['gbar_' + skey + '_L2Basket_nmda'],
                          0.1, p['sigma_t_' + skey]),
            'L5_pyramidal': (p['gbar_' + skey + '_L5Pyr_ampa'],
                             p['gbar_' + skey + '_L5Pyr_nmda'],
                             1., p['sigma_t_' + skey]),
            'L5_basket': (p['gbar_' + skey + '_L5Basket_ampa'],
                          p['gbar_' + skey + '_L5Basket_nmda'],
                          1., p['sigma_t_' + skey]),
            'prng_seedcore': int(p['prng_seedcore_' + skey]),
            'lamtha': 3.,
            'loc': 'proximal',
            'sync_evinput': p['sync_evinput'],
            'threshold': p['threshold'],
            'numspikes': p['numspikes_' + skey]
        }

    # Create distal evoked response parameters
    # f_input needs to be defined as 0
    for i in range(ndist):
        skey = 'evdist_' + str(i + 1)
        p_unique['evdist' + str(i + 1)] = {
            't0': p['t_' + skey],
            'L2_pyramidal': (p['gbar_' + skey + '_L2Pyr_ampa'],
                             p['gbar_' + skey + '_L2Pyr_nmda'],
                             0.1, p['sigma_t_' + skey]),
            'L5_pyramidal': (p['gbar_' + skey + '_L5Pyr_ampa'],
                             p['gbar_' + skey + '_L5Pyr_nmda'],
                             0.1, p['sigma_t_' + skey]),
            'L2_basket': (p['gbar_' + skey + '_L2Basket_ampa'],
                          p['gbar_' + skey + '_L2Basket_nmda'],
                          0.1, p['sigma_t_' + skey]),
            'prng_seedcore': int(p['prng_seedcore_' + skey]),
            'lamtha': 3.,
            'loc': 'distal',
            'sync_evinput': p['sync_evinput'],
            'threshold': p['threshold'],
            'numspikes': p['numspikes_' + skey]
        }

    # this needs to create many feeds
    # (amplitude, delay, mu, sigma). ordered this way to preserve compatibility
    # NEW: note double weight specification since only use ampa for gauss
    # inputs
    p_unique['extgauss'] = {
        'stim': 'gaussian',
        'L2_basket': (p['L2Basket_Gauss_A_weight'],
                      p['L2Basket_Gauss_A_weight'],
                      1., p['L2Basket_Gauss_mu'],
                      p['L2Basket_Gauss_sigma']),
        'L2_pyramidal': (p['L2Pyr_Gauss_A_weight'],
                         p['L2Pyr_Gauss_A_weight'],
                         0.1, p['L2Pyr_Gauss_mu'], p['L2Pyr_Gauss_sigma']),
        'L5_basket': (p['L5Basket_Gauss_A_weight'],
                      p['L5Basket_Gauss_A_weight'],
                      1., p['L5Basket_Gauss_mu'], p['L5Basket_Gauss_sigma']),
        'L5_pyramidal': (p['L5Pyr_Gauss_A_weight'],
                         p['L5Pyr_Gauss_A_weight'],
                         1., p['L5Pyr_Gauss_mu'], p['L5Pyr_Gauss_sigma']),
        'lamtha': 100.,
        'prng_seedcore': int(p['prng_seedcore_extgauss']),
        'loc': 'proximal',
        'threshold': p['threshold']
    }

    check_pois_synkeys(p)

    # Poisson distributed inputs to proximal
    # NEW: setting up AMPA and NMDA for Poisson inputs; why delays differ?
    p_unique['extpois'] = {
        'stim': 'poisson',
        'L2_basket': (p['L2Basket_Pois_A_weight_ampa'],
                      p['L2Basket_Pois_A_weight_nmda'],
                      1., p['L2Basket_Pois_lamtha']),
        'L2_pyramidal': (p['L2Pyr_Pois_A_weight_ampa'],
                         p['L2Pyr_Pois_A_weight_nmda'],
                         0.1, p['L2Pyr_Pois_lamtha']),
        'L5_basket': (p['L5Basket_Pois_A_weight_ampa'],
                      p['L5Basket_Pois_A_weight_nmda'],
                      1., p['L5Basket_Pois_lamtha']),
        'L5_pyramidal': (p['L5Pyr_Pois_A_weight_ampa'],
                         p['L5Pyr_Pois_A_weight_nmda'],
                         1., p['L5Pyr_Pois_lamtha']),
        'lamtha': 100.,
        'prng_seedcore': int(p['prng_seedcore_extpois']),
        't_interval': (p['t0_pois'], p['T_pois']),
        'loc': 'proximal',
        'threshold': p['threshold']
    }

    return p_common, p_unique


# Takes two dictionaries (d1 and d2) and compares the keys in d1 to those in d2
# if any match, updates the (key, value) pair of d1 to match that of d2
# not real happy with variable names, but will have to do for now
def compare_dictionaries(d1, d2):
    # iterate over intersection of key sets (i.e. any common keys)
    for key in d1.keys() and d2.keys():
        # update d1 to have same (key, value) pair as d2
        d1[key] = d2[key]

    return d1


def _any_positive_weights(drive):
    """ Checks a drive for any positive weights. """
    weights = (list(drive['weights_ampa'].values()) +
               list(drive['weights_nmda'].values()))
    if any([val > 0 for val in weights]):
        return True
    else:
        return False


def remove_nulled_drives(net):
    """Removes drives from network if they have been given null parameters.

    Legacy param files contained parameter placeholders for non-functional
    drives. These drives were nulled by assigning values outside typical
    ranges. This function removes drives on the following conditions:
        1. Start time is larger than stop time
        2. All weights are non-positive

    Parameters
    ----------
    net : Network object

    Returns
    -------
    net : Network object

    """
    from .network import pick_connection

    net = deepcopy(net)
    drives_copy = net.external_drives.copy()

    extras = dict()
    for drive_name, drive in net.external_drives.items():
        conn_indices = pick_connection(net, src_gids=drive_name)

        space_constant = net.connectivity[conn_indices[0]]['nc_dict']['lamtha']
        probability = net.connectivity[conn_indices[0]]['probability']

        extras[drive_name] = {'space_constant': space_constant,
                              'probability': probability}

    net.clear_drives()
    for drive_name, drive in drives_copy.items():
        # Do not add drive if tstart is > tstop, or negative
        t_start = drive['dynamics'].get('tstart')
        t_stop = drive['dynamics'].get('tstop')
        if (t_start is not None and t_stop is not None and
                ((t_start > t_stop) or
                 (t_start < 0) or
                 (t_stop < 0))):
            continue
        # Do not add if all 0 weights
        elif not _any_positive_weights(drive):
            continue
        else:
            # Set n_drive_cells to 'n_cells' if equal to max number of cells
            if drive['cell_specific']:
                drive['n_drive_cells'] = 'n_cells'
            net._attach_drive(drive['name'], drive, drive['weights_ampa'],
                              drive['weights_nmda'], drive['location'],
                              extras[drive_name]['space_constant'],
                              drive['synaptic_delays'],
                              drive['n_drive_cells'], drive['cell_specific'],
                              extras[drive_name]['probability'])
    return net


def convert_to_json(params_fname,
                    out_fname,
                    include_drives=True,
                    overwrite=True):
    """Converts legacy json or param format to hierarchical json format

    Parameters
    ----------
    params_fname : str or Path
        Path to file
    out_fname: str
        Path to output
    include_drives: bool, default=True
        Include drives from params file
    overwrite: bool, default=True
        Overwrite file
    Returns
    -------
    None
    """
    from .network_models import jones_2009_model

    # Validate inputs
    _validate_type(params_fname, (str, Path), 'params_fname')
    _validate_type(out_fname, (str, Path), 'out_fname')
    # Convert to Path
    params_fname = Path(params_fname)
    out_fname = Path(out_fname)
    params_suffix = params_fname.suffix.lower().split('.')[-1]

    # Add suffix if not supplied
    if out_fname.suffix != '.json':
        out_fname = out_fname.with_suffix('.json')

    net = jones_2009_model(params=read_params(params_fname),
                           add_drives_from_params=include_drives,
                           legacy_mode=(True if params_suffix == 'param'
                                        else False),
                           )

    # Remove drives that have null attributes
    net = remove_nulled_drives(net)

    net.write_configuration(fname=out_fname,
                            overwrite=overwrite,
                            )
    return


# debug test function
if __name__ == '__main__':
    fparam = 'param/debug.param'
