from __future__ import print_function, division
import numpy as np
import moose
import random
random.seed(1)

from . import logutil
from .util import distance_mapping
from .add_channel import addOneChan
log = logutil.Logger()

#NAME_NECK and NAME_HEAD are used in calcium.py to add calcium objects to spines
#If you get rid of them, you have to change calcium.py
NAME_NECK = "neck"
NAME_HEAD = "head"

'''The default behavior for spines is to assume no explicit spines and
compensate for the spineDensity parameter in initial model construction. This
file includes the compensation function, called in cellproto. Then, if any
explicit spines are to be included, reverse compensation is performed.
'''

def setSpineCompParams(model, comp,compdia,complen,RA,RM,CM):

    comp.diameter = compdia
    comp.length = complen

    XArea = np.pi*comp.diameter*comp.diameter/4

    circumf = np.pi*compdia
    log.debug('Xarea,circumf of {}, {}, {} CM {} {}',
              comp.path, XArea, circumf,
              CM,np.pi*comp.diameter*comp.length)
    comp.Ra = RA*comp.length/XArea
    comp.Rm = RM/(np.pi*comp.diameter*comp.length)
    cm = CM*np.pi*comp.diameter*comp.length
    if cm < 1e-15:
        cm = 1e-15
    comp.Cm = cm
    comp.Em = model.SpineParams.spineELEAK
    comp.initVm = model.SpineParams.spineEREST


def setPassiveSpineParams(model,container,name_soma):
    '''Sets the Spine Params for RM, CM, RA, from global values if NONE '''
    soma = moose.element(container+'/'+name_soma)
    if soma.length == 0:
         SA = np.pi*soma.diameter**2
         len_by_XA=1/(np.pi * soma.diameter/8) #eqn from Hendrickson & Jaeger 2010
    else:
        SA =np.pi * soma.diameter * soma.length
        len_by_XA=soma.length / (np.pi * soma.diameter * soma.diameter/4) #4 converts dia to radius
    globalRM = soma.Rm * SA
    globalCM = soma.Cm / SA
    globalRA = soma.Ra / len_by_XA
    globalELEAK = soma.Em
    globalEREST = soma.initVm
    if model.SpineParams.spineRM is None:
        model.SpineParams.spineRM = globalRM
        log.debug('Setting spineRM to globalRM = {}', globalRM)
    if model.SpineParams.spineCM is None:
        model.SpineParams.spineCM = globalCM
        log.debug('Setting spineCM to globalCM = {}', globalCM)
    if model.SpineParams.neckRA is None:
        model.SpineParams.neckRA = globalRA
        log.debug('Setting neckRA to globalRA = {}', globalRA)
    if model.SpineParams.headRA is None:
        model.SpineParams.headRA = globalRA
        log.debug('Setting headRA to globalRA = {}', globalRA)
    if model.SpineParams.spineELEAK is None:
        model.SpineParams.spineELEAK = globalELEAK
        log.debug('Setting spineELEAK to globalELEAK = {}', globalELEAK)
    if model.SpineParams.spineEREST is None:
        model.SpineParams.spineEREST = globalEREST
        log.debug('Setting spineEREST to globalEREST = {}', globalEREST)

from scipy.linalg import norm

def makeSpine(model, parentComp, compName,index,frac,SpineParams,randomAngles=True):
    #frac is where along the compartment the spine is attached
    #unfortunately, these values specified in the .p file are not accessible
    neck_path = '{}/{}{}{}'.format(parentComp.path, compName, index, NAME_NECK)
    neck = moose.Compartment(neck_path)
    log.debug('{} at {} x,y,z={},{},{}', neck.path, frac, parentComp.x, parentComp.y, parentComp.z)
    moose.connect(parentComp,'raxial',neck,'axial','Single')
    #evenly distribute the spines along the parent compartment
    x=parentComp.x0+ frac * (parentComp.x - parentComp.x0)
    y=parentComp.y0+ frac * (parentComp.y - parentComp.y0)
    z=parentComp.z0+ frac * (parentComp.z - parentComp.z0)
    neck.x0, neck.y0, neck.z0 = x, y, z
    from scipy.linalg import norm

    if randomAngles:
        # random angles for visualization
        dendvect = np.array([parentComp.x, parentComp.y, parentComp.z]) - np.array([parentComp.x0, parentComp.y0, parentComp.z0])
        dendvmag = norm(dendvect)
        dendunitvec = dendvect/dendvmag
        not_v = np.random.random(3)
        not_v /= norm(not_v)
        while (dendunitvec == not_v).all():
            not_v = np.random.random(3)
            not_v /= norm(not_v)
        #make vector perpendicular to v
        n1 = np.cross(dendunitvec, not_v)
        #normalize n1
        n1 /= norm(n1)
        neck.x, neck.y, neck.z = SpineParams.necklen*n1
        #print(neck.x, neck.y, neck.z)
    else:
        #could pass in an angle and use cos and sin to set y and z
        neck.x, neck.y, neck.z = x, y + SpineParams.necklen, z
    setSpineCompParams(model, neck,SpineParams.neckdia,SpineParams.necklen,SpineParams.neckRA,SpineParams.spineRM,SpineParams.spineCM)

    head_path = '{}/{}{}{}'.format(parentComp.path, compName, index, NAME_HEAD)
    head = moose.Compartment(head_path)
    moose.connect(neck, 'raxial', head, 'axial', 'Single')
    head.x0, head.y0, head.z0 = neck.x, neck.y, neck.z
    head.x, head.y, head.z = SpineParams.headlen*np.array([head.x0, head.y0, head.z0])/norm(np.array([head.x0, head.y0, head.z0]))
    #head.x0, head.y0 + SpineParams.headlen, head.z0

    setSpineCompParams(model, head,SpineParams.headdia,SpineParams.headlen,SpineParams.headRA,SpineParams.spineRM,SpineParams.spineCM)
    return head, neck


def compensate_for_spines(model,comp,name_soma):#,total_spine_surface,surface_area):
    setPassiveSpineParams(model,comp.parent.name,name_soma)
    SpineParams = model.SpineParams
    distance_mapped_spineDensity = {(SpineParams.spineStart,SpineParams.spineEnd):SpineParams.spineDensity}
    if SpineParams.spineDensity == 0:
        return
    if not compensate_for_spines.has_been_called:
        print('Compensating for spines using SpineParams.spineDensity = ' +
              str(SpineParams.spineDensity) +
              ' ; Set to zero skip spine compensation.' )
        compensate_for_spines.has_been_called = True
    dist = (comp.x**2+comp.y**2+comp.z**2)**0.5
    if name_soma not in comp.path and (SpineParams.spineEnd > dist > SpineParams.spineStart):
        #determine the number of spines
        spineDensity = distance_mapping(distance_mapped_spineDensity,comp)
        if spineDensity < 0 or spineDensity > 20e6:
            print('SpineDensity {} may be unrealistic; check function'.format(spineDensity))
        numSpines = int(np.round(spineDensity*comp.length))
        #print('Spine Density = ' + str(spineDensity), 'NumSpines={}'.format(numSpines))

        #if spine density is low (less than 1 per comp) use random number to determine whether to add a spine
        if not numSpines:
             rand = random.random()
             if rand > spineDensity*comp.length:
                 numSpines = 1
        #calculate total surface area of the added spines
        single_spine_surface = spine_surface(SpineParams)
        total_spine_surface = numSpines*single_spine_surface
        surface_area = comp.diameter*comp.length*np.pi

        ## Compensate RM and CM
        old_Cm = comp.Cm
        old_Rm = comp.Rm
        scaling_factor = (surface_area+total_spine_surface)/surface_area

        comp.Cm = old_Cm * scaling_factor
        comp.Rm = old_Rm / scaling_factor

        ## Additionally, compensate for ion channels in spines
        # Flatten the nested chan list:
        chan_dict = {}
        for c in SpineParams.spineChanList:
            chan_dict.update(c)
        # Get the conductance for each channel:
        for chanpath,mult in chan_dict.items():
            if moose.exists(comp.path+'/'+chanpath):
                chan = moose.element(comp.path+'/'+chanpath)
                old_gbar = chan.Gbar/surface_area
                spine_dend_gbar_ratio = 1.0 #TODO: Change if spine has different gbar than dendrite
                gbar_factor = (surface_area + spine_dend_gbar_ratio*total_spine_surface)/surface_area
                new_gbar = old_gbar*gbar_factor
                chan.Gbar = new_gbar*surface_area
                #if 'CaR' in chan.path and 'tertdend1_2' in chan.path:
                #    print('Compensating ' + chan.path + ' from old gbar: ' + str(old_gbar) + ' to new: ' + str(new_gbar))
#Initialize function attribute:
compensate_for_spines.has_been_called = False

def reverse_compensate_for_explicit_spines(model,comp,explicit_spine_surface,surface_area):
    old_Cm = comp.Cm
    old_Rm = comp.Rm
    scaling_factor = (surface_area+explicit_spine_surface)/surface_area

    comp.Cm = old_Cm / scaling_factor # Note, opposite signs from spine compensation
    comp.Rm = old_Rm * scaling_factor
    ## Additionally, reverse compensate for ion channels in spines
    # Flatten the nested chan list:
    SpineParams = model.SpineParams
    chan_dict = {}
    for c in SpineParams.spineChanList:
        chan_dict.update(c)
    # Get the conductance for each channel:
    for chanpath,mult in chan_dict.items():
        if moose.exists(comp.path+'/'+chanpath):
            chan = moose.element(comp.path+'/'+chanpath)
            old_gbar = chan.Gbar/surface_area
            spine_dend_gbar_ratio = 1.0 #TODO: Change if spine has different gbar than dendrite
            gbar_factor = (surface_area + spine_dend_gbar_ratio*explicit_spine_surface)/surface_area
            new_gbar = old_gbar/gbar_factor
            chan.Gbar = new_gbar*surface_area
            #if 'CaR' in chan.path and 'tertdend1_2' in chan.path:
            #    print('Reverse Compensating ' + chan.path + ' from old gbar: ' + str(old_gbar) + ' to new: ' + str(new_gbar))


def spine_surface(SpineParams):
    headdia = SpineParams.headdia
    headlen = SpineParams.headlen
    neckdia = SpineParams.neckdia
    necklen = SpineParams.necklen
    surface = headdia*headlen + neckdia*necklen

    return surface*np.pi

def getChildren(parentname,childrenlist):

    children = moose.element(parentname).neighbors['axialOut']
    if len(children):
        for child in children:
            childrenlist.append(child.name)
            getChildren(child,childrenlist)

def addSpines(model, container,ghkYN,name_soma,neuron_object):
    distance_mapped_spineDensity = {(model.SpineParams.spineStart,model.SpineParams.spineEnd):model.SpineParams.spineDensity}
    headarray=[]
    # Sets Spine Params to global values for RM, CM, etc. if value is None:
    setPassiveSpineParams(model,container,name_soma)
    SpineParams = model.SpineParams
    suma = 0

    modelcond = model.Condset[container]
    single_spine_surface = spine_surface(SpineParams)

    # if clustering spines, call those functions here and return early
    if "ClusteringParams" in SpineParams:
        possible_spines = getPossibleSpines(model, container,ghkYN,name_soma,neuron_object)
        spine_to_spine_dists = possible_spine_to_spine_distances(model, possible_spines,neuron_object)
        chosen_spine_clusters,chosen_spines_in_each_cluster,clustered_spine_index = choose_spine_clusters(model, possible_spines, spine_to_spine_dists,SpineParams.ClusteringParams['n_clusters'], SpineParams.ClusteringParams['cluster_length'], SpineParams.ClusteringParams['n_spines_per_cluster'])
        all_possible_spine_info={ps['head_path']:(ps['x'],ps['y'],ps['z']) for ps in possible_spines} #maybe this should be ordered list?
        if len(spine_to_spine_dists)==len(possible_spines)+1:
            soma = moose.element(container+'/'+name_soma)
            print('adding index for soma to s2sd',soma.path)
            all_possible_spine_info[soma.path]=(0,0,0) #add soma
        added_spines={sp:[possible_spines[x]['head_path'] for x in chosen_spines_in_each_cluster[i]] for i,sp in enumerate(chosen_spine_clusters)}
        for sp in clustered_spine_index:
            head =add_one_spine_from_spine_info(possible_spines[sp],model,SpineParams,container,ghkYN,name_soma)
            #print('test adding one spine of cluster')
            headarray.append(head)
        print('prep for auto file name',model.morph_file, container,len(headarray),'spines created')
        fname=model.morph_file[container].split('.p')[0] #container is ntype
        np.savez(fname+'_s2sdist', s2sd=spine_to_spine_dists,index=all_possible_spine_info) #needed for analysis later.  Probably need to save in different directory
        return headarray
    else:
        parentComp = container+'/'+SpineParams.spineParent
        print('Adding spines to parent: ' + parentComp)
        if not moose.exists(parentComp):
            raise Exception(parentComp + ' Does not exist in Moose model!')
        compList = [SpineParams.spineParent]
        getChildren(parentComp,compList)

        for comp in moose.wildcardFind(container + '/#[TYPE=Compartment]'):
            dist = (comp.x**2+comp.y**2+comp.z**2)**0.5
            if name_soma not in comp.path and comp.name in compList and (SpineParams.spineEnd > dist > SpineParams.spineStart):
                #determine the number of spines
                try:
                    #If SpineParams has this, use this density
                    density=SpineParams.explicitSpineDensity
                except KeyError:
                    #Else, just use the actual density value
                    density=distance_mapping(distance_mapped_spineDensity,comp)
                numSpines = int(np.round(density*comp.length))

                #if spine density is low (less than 1 per comp) use random number to determine whether to add a spine
                if not numSpines:
                    rand = random.random()
                    if rand > density*comp.length:
                        numSpines = 1
                        suma += 1
                #calculate total surface area of the added spines
                total_spine_surface = numSpines*single_spine_surface
                surface_area = comp.diameter*comp.length*np.pi

                # if SpineParams.compensationSpineDensity:
                #     compensation_spine_surface = int(np.round(SpineParams.compensationSpineDensity*comp.length))*single_spine_surface
                #     decompensate_compensate_for_spines(comp,total_spine_surface,surface_area,compensation_spine_surface)
                # else:
                #increase resistance according to the spines that should be there but aren't
                reverse_compensate_for_explicit_spines(model,comp,total_spine_surface,surface_area)

                #spineSpace = comp.length/(numSpines+1)
                #for each spine, make a spine and possibly compensate for its surface area
                for index in range(numSpines):
                    frac = (index+0.5)/numSpines
                    #print comp.path,"Spine:", index, "located:", frac
                    head,neck = makeSpine(model, comp, 'sp',index, frac, SpineParams)
                    headarray.append(head)
                    if SpineParams.spineChanList:
                        if ghkYN:
                            ghkproto=moose.element('/library/ghk')
                            ghk=moose.copy(ghkproto,comp,'ghk')[0]
                            moose.connect(ghk,'channel',comp,'channel')
                        chan_dict = {}
                        for c in SpineParams.spineChanList:
                            chan_dict.update(c)
                        for chanpath,mult in chan_dict.items():
                            cond = mult#*distance_mapping(modelcond[chanpath],head)
                            if cond > 0:
                                log.debug('Testing Cond If {} {}', chanpath, cond)
                                calciumPermeable = model.Channels[chanpath].calciumPermeable
                                addOneChan(chanpath,cond,head,ghkYN,calciumPermeable=calciumPermeable)
                #end for index
        #end for comp

        log.info('{} spines created in {}', len(headarray), container)
        return headarray

def getPossibleSpines(model, container,ghkYN,name_soma,neuron_object):
    possible_spine_list = [] # Collect every potential spine with info needed to make that spine in another function.
        
    distance_mapped_spineDensity = {(model.SpineParams.spineStart,model.SpineParams.spineEnd):model.SpineParams.spineDensity}
    SpineParams = model.SpineParams
    suma = 0
    parentComp = container+'/'+SpineParams.spineParent
    print('Adding spines to parent: ' + parentComp)
    if not moose.exists(parentComp):
        raise Exception(parentComp + ' Does not exist in Moose model!')
    compList = [SpineParams.spineParent]
    getChildren(parentComp,compList)

    for comp in moose.wildcardFind(container + '/#[ISA=CompartmentBase]'):
        #print(comp)
        dist = (comp.x**2+comp.y**2+comp.z**2)**0.5
        if name_soma not in comp.path and comp.name in compList and (SpineParams.spineEnd > dist > SpineParams.spineStart):
            #determine the number of possible spines using distance mapped density
            density=distance_mapping(distance_mapped_spineDensity,comp)
            numSpines = int(np.round(density*comp.length))

            #if spine density is low (less than 1 per comp) use random number to determine whether to add a spine
            if not numSpines:
                 rand = random.random()
                 if rand > density*comp.length:
                     numSpines = 1
                     suma += 1
            #calculate total surface area of the added spines
            #total_spine_surface = numSpines*single_spine_surface
            #surface_area = comp.diameter*comp.length*np.pi

            # if SpineParams.compensationSpineDensity:
            #     compensation_spine_surface = int(np.round(SpineParams.compensationSpineDensity*comp.length))*single_spine_surface
            #     decompensate_compensate_for_spines(comp,total_spine_surface,surface_area,compensation_spine_surface)
            # else:
            #increase resistance according to the spines that should be there but aren't
            
            ### Do this when actually adding the spines
            #reverse_compensate_for_explicit_spines(model,comp,total_spine_surface,surface_area)

            #spineSpace = comp.length/(numSpines+1)
            #for each spine, make a spine and possibly compensate for its surface area
            for index in range(numSpines):
                frac = (index+0.5)/numSpines
                #print comp.path,"Spine:", index, "located:", frac
                #head,neck = makeSpine(model, comp, 'sp',index, frac, SpineParams)
                spineInfo = makeSpineInfo(model, comp, 'sp',index, frac, SpineParams)
                #headarray.append(head)
                possible_spine_list.append(spineInfo)
                # if SpineParams.spineChanList:
                #     if ghkYN:
                #         ghkproto=moose.element('/library/ghk')
                #         ghk=moose.copy(ghkproto,comp,'ghk')[0]
                #         moose.connect(ghk,'channel',comp,'channel')
                #     chan_dict = {}
                #     for c in SpineParams.spineChanList:
                #         chan_dict.update(c)
                #     for chanpath,mult in chan_dict.items():
                #         cond = mult#*distance_mapping(modelcond[chanpath],head)
                #         if cond > 0:
                #             log.debug('Testing Cond If {} {}', chanpath, cond)
                #             calciumPermeable = model.Channels[chanpath].calciumPermeable
                #             addOneChan(chanpath,cond,head,ghkYN,calciumPermeable=calciumPermeable)
            #end for index
    #end for comp

    #log.info('{} spines created in {}', len(headarray), container)
    #return headarray
    return possible_spine_list

def makeSpineInfo(model, parentComp, compName,index,frac,SpineParams,randomAngles=True):
    #print('making spine info')
    spine_info = {}
    spine_info['parentComp'] = parentComp
    spine_info['compName']= compName

    #frac is where along the compartment the spine is attached
    #unfortunately, these values specified in the .p file are not accessible
    neck_path = '{}/{}{}{}'.format(parentComp.path, compName, index, NAME_NECK)
    spine_info['neck_path'] = neck_path
    #neck = moose.Compartment(neck_path)
    #log.debug('{} at {} x,y,z={},{},{}', neck.path, frac, parentComp.x, parentComp.y, parentComp.z)
    #moose.connect(parentComp,'raxial',neck,'axial','Single')
    #evenly distribute the spines along the parent compartment
    x=parentComp.x0+ frac * (parentComp.x - parentComp.x0)
    y=parentComp.y0+ frac * (parentComp.y - parentComp.y0)
    z=parentComp.z0+ frac * (parentComp.z - parentComp.z0)
    spine_info['x'] = x
    spine_info['y'] = y
    spine_info['z'] = z
    #neck.x0, neck.y0, neck.z0 = x, y, z
    #setSpineCompParams(model, neck,SpineParams.neckdia,SpineParams.necklen,SpineParams.neckRA,SpineParams.spineRM,SpineParams.spineCM)

    head_path = '{}/{}{}{}'.format(parentComp.path, compName, index, NAME_HEAD)
    spine_info['head_path'] = head_path
    #head = moose.Compartment(head_path)
    #moose.connect(neck, 'raxial', head, 'axial', 'Single')
    #head.x0, head.y0, head.z0 = neck.x, neck.y, neck.z
    #head.x, head.y, head.z = SpineParams.headlen*np.array([head.x0, head.y0, head.z0])/norm(np.array([head.x0, head.y0, head.z0]))
    #head.x0, head.y0 + SpineParams.headlen, head.z0

    #setSpineCompParams(model, head,SpineParams.headdia,SpineParams.headlen,SpineParams.headRA,SpineParams.spineRM,SpineParams.spineCM)
    return spine_info #head, neck

def possible_spine_to_spine_distances(model, possible_spines,neuron_object):
    from moose_nerp.prototypes import spatiotemporalInputMapping as stim
    neuron = neuron_object#list(model.neurons.values())[0][0]
    bd = stim.getBranchDict(neuron)
    comp_to_branch_dict = stim.mapCompartmentToBranch(neuron)
    
    def compute_spine_to_spine_dist(spine, other_spine,print_info=False):
        '''Compute the path distance along dendritic tree between any two spines'''
        # get parent compartment of spine
        spine_parents = [spine['parentComp'], other_spine['parentComp']]

        # get the branch of the spine_parent
        spine_branches = [comp_to_branch_dict[sp.path] for sp in spine_parents]
        branch_paths = spine_branches[0]['BranchPath'], spine_branches[1]['BranchPath']
        # if on same branch
        if spine_branches[0]==spine_branches[1]:
            # if on same compartment:
            if spine_parents[0]==spine_parents[1]:
                spine_to_spine_dist = np.sqrt((spine['x'] - other_spine['x'])**2 + (spine['y'] - other_spine['y'])**2 + (spine['z'] - other_spine['z'])**2)
            # else if on same branch but not same compartment:
            else:
                compdistances = [bd[sb['Branch']]['CompDistances'] for sb in spine_branches]
                complists = [bd[sb['Branch']]['CompList'] for sb in spine_branches]
                compindexes = [cl.index(spine_parent.path) for cl,spine_parent in zip(complists, spine_parents)]
                comp_to_comp_distance = np.abs(compdistances[0][compindexes[0]] - compdistances[1][compindexes[1]])
                spine_to_spine_dist = comp_to_comp_distance
        # else if on different branches, find common parent branch first
        else:
            for a,b in zip(branch_paths[0], branch_paths[1]):
                #print(a,b)
                if a == b:
                    common_parent = a
            common_parent_distance = bd[common_parent]['CompDistances'][0]
            if print_info:
                print('common parent is ', common_parent, 'common_parent_distance is ', common_parent_distance)
            allcompdistances = [bd[sb['Branch']]['CompDistances'] for sb in spine_branches]
            complists = [bd[sb['Branch']]['CompList'] for sb in spine_branches]
            compindexes = [cl.index(spine_parent.path) for cl,spine_parent in zip(complists, spine_parents)]
            compdistances = [allcompdistances[0][compindexes[0]], allcompdistances[1][compindexes[1]]]
            comp_to_comp_distance = (compdistances[0]-common_parent_distance) + (compdistances[1]-common_parent_distance)
            if print_info:
                print('compdistances',compdistances,'comp_to_comp_distance', comp_to_comp_distance)
            spine_to_spine_dist = comp_to_comp_distance
        return spine_to_spine_dist
    

    spine_to_spine_dist_array = np.zeros((len(possible_spines)+1, len(possible_spines)+1)) #+1 for adding spine to soma distance
    spine_index=[]
    import itertools
    for spine_pairs in itertools.combinations(range(len(possible_spines)), 2):
        spine_dist = compute_spine_to_spine_dist(possible_spines[spine_pairs[0]],possible_spines[spine_pairs[1]])
        spine_to_spine_dist_array[spine_pairs[0], spine_pairs[1]] = spine_dist
        spine_to_spine_dist_array[spine_pairs[1], spine_pairs[0]] = spine_dist
    missing=[]
    for spnum,spine in enumerate(possible_spines):
        spine_parent=spine['parentComp']
        if spine_parent.path in comp_to_branch_dict.keys():
            spine_branch = comp_to_branch_dict[spine_parent.path]
            allcompdistances = bd[spine_branch['Branch']]['CompDistances']
            complist = bd[spine_branch['Branch']]['CompList']
            compindex = complist.index(spine_parent.path)
            spine_to_spine_dist_array[spnum,-1]=allcompdistances[compindex]
            spine_to_spine_dist_array[-1,spnum]=allcompdistances[compindex]
        else:
            if spine_parent.path not in missing:
                missing.append(spine_parent.path)
            #print('spine',spine.path,'parent comp not in comp_to_branch_dict', comp_to_branch_dict.keys())
    if len(missing):
        print('these comps not in comp_to_branch_dict',missing)

    return spine_to_spine_dist_array

def choose_spine_clusters(model, possible_spines, spine_to_spine_dist_array, n_clusters, cluster_length, n_spines_per_cluster):
    # randomly choose n_clusters spines from possible spines
    # ensure the randomly chosen spines for each different cluster are well distributed 
    chosen_spine_clusters = []
    possible_spine_index = list(range(len(possible_spines)))
    for i in range(n_clusters):
        validated_spine = False
        while not validated_spine:
            chosen_spine = np.random.choice(possible_spine_index,1)
            possible_spine_index.remove(chosen_spine)
            # make sure at least n_spines_per_cluster within cluster_length of chosen spine
            num_spines_within_length = np.argwhere(spine_to_spine_dist_array[chosen_spine, :]<cluster_length).shape[0]
            if num_spines_within_length < n_spines_per_cluster:
                print('too few spines nearby for cluster choose again')
                continue
            # Check if chosen spine is at least 3xcluster_length distance from all other clusters
            if all(spine_to_spine_dist_array[chosen_spine, chosen_spine_clusters] > 3*cluster_length):
                chosen_spine_clusters.append(chosen_spine[0])
                validated_spine = True
                print('chosen spine cluster length ', len(chosen_spine_clusters)) 
            else:
                print('too close to other spines in cluster choosing again')
    
    # Now randomly choose n_spines_percluster within cluster_length of each cluster
    chosen_spines_in_each_cluster = []
    for i in range(n_clusters):
        origin_spine = chosen_spine_clusters[i]
        choices = np.random.choice(np.where(
            (spine_to_spine_dist_array[origin_spine,:]<cluster_length) &
            (spine_to_spine_dist_array[origin_spine,:]>0))[0],
            size=n_spines_per_cluster-1, replace=False)
        choices = list(choices)
        choices.append(origin_spine)
        chosen_spines_in_each_cluster.append(choices)
    
    all_chosen_spines = [s for clus in chosen_spines_in_each_cluster for s in clus]
    return chosen_spine_clusters,chosen_spines_in_each_cluster,all_chosen_spines

def add_one_spine_from_spine_info(spine_info, model,SpineParams,container,ghkYN,name_soma):
    compName = spine_info['compName']
    neck_path = spine_info['neck_path']
    parentComp = spine_info['parentComp']
    neck = moose.Compartment(neck_path)
    #log.debug('{} at {} x,y,z={},{},{}', neck.path, frac, parentComp.x, parentComp.y, parentComp.z)
    moose.connect(parentComp,'raxial',neck,'axial','Single')
    neck.x0, neck.y0, neck.z0 = spine_info['x'], spine_info['y'], spine_info['z']
    neck.x, neck.y, neck.z = neck.x0, neck.y0 + SpineParams.necklen, neck.z0
    setSpineCompParams(model, neck,SpineParams.neckdia,SpineParams.necklen,SpineParams.neckRA,SpineParams.spineRM,SpineParams.spineCM)
    head_path = spine_info['head_path']
    head = moose.Compartment(head_path)
    moose.connect(neck, 'raxial', head, 'axial', 'Single')
    head.x0, head.y0, head.z0 = neck.x, neck.y, neck.z
    head.x, head.y, head.z = SpineParams.headlen*np.array([head.x0, head.y0, head.z0])/norm(np.array([head.x0, head.y0, head.z0]))

    setSpineCompParams(model, head,SpineParams.headdia,SpineParams.headlen,SpineParams.headRA,SpineParams.spineRM,SpineParams.spineCM)

    surface_area = parentComp.diameter*parentComp.length*np.pi
    reverse_compensate_for_explicit_spines(model,parentComp,spine_surface(SpineParams),surface_area)
    # add channel code for single spine here
    if SpineParams.spineChanList:
        if ghkYN:
            ghkproto=moose.element('/library/ghk')
            ghk=moose.copy(ghkproto,comp,'ghk')[0]
            moose.connect(ghk,'channel',comp,'channel')
        chan_dict = {}
        for c in SpineParams.spineChanList:
            chan_dict.update(c)
        for chanpath,mult in chan_dict.items():
            cond = mult#*distance_mapping(modelcond[chanpath],head)
            if cond > 0:
                log.debug('Testing Cond If {} {}', chanpath, cond)
                calciumPermeable = model.Channels[chanpath].calciumPermeable
                addOneChan(chanpath,cond,head,ghkYN,calciumPermeable=calciumPermeable)
    return head