# -*- coding:utf-8 -*-
from __future__ import print_function, division
import numpy as np
import matplotlib.pyplot as plt
from pprint import pprint
import moose
import logging
import inspect


from moose_nerp.prototypes import (cell_proto,
                                   calcium,
                                   clocks,
                                   inject_func,
                                   tables,
                                   plasticity_test,
                                   logutil,
                                   util,
                                   standard_options,
                                   constants,
                                   print_params)
from moose_nerp.graph import plot_channel, neuron_graph, spine_graph

def setupLogging(model, level = logging.INFO):
    ### logging.basicConfig(level=level) ### basicConfig only works in __main__
    logging.getLogger().setLevel(level) # Setting this works outside of __main__
    model.log = logutil.Logger()
    return model.log

@util.call_counter
def setupOptions(model, **kwargs):
    '''Can be called with no arguments except model. This will use the defaults
    in param_sim.py param_model_defaults.py and apply no overrides.

    Optionally, can pass any keyword arguments that are parameter names in
    param_sim (e.g. fname, plotcomps, logging_level, etc.) to override them.

    This function also handles any command line arguments. When run from command
    line, any command line options will take precedence over param_sim and
    param_model_defaults.

    Function is wrapped with a function that counts calls to enable checking if
    options have already been set up.
    '''
    # Warn if overwriting prior setupOptions
    if setupOptions.calls > 1:
        model.log.warning('''setupOptions has already been called. Overwriting
                          prior call with new options''')

    # Get the option_parsers
    param_sim_parser, model_parser = standard_options.standard_options()

    param_sim = model.param_sim
    ###### apply any kwargs that are param_sim overrides. #####
    # Find set of all kwargs that match param_sim variables
    param_sim_overrides = set(set(kwargs.keys()) & set(vars(param_sim).keys()))
    # Override each param_sim parameter in kwargs and pop item from kwargs
    for k in param_sim_overrides:
        setattr(param_sim, k, kwargs.pop(k))

    # Command line args should have precedence--call here to update param_sim
    # with any command line options after applying param_sim_overrides above
    # Passing existing namespace into parse_known_args will only set attributes
    # for command line arguments that are explicitly passed; no defaults are set
    param_sim, unknown_args = param_sim_parser.parse_known_args(namespace = param_sim)
    # parsing model params separately for setting spineYN, calYN, etc. from command line
    model, _ = model_parser.parse_known_args(unknown_args, namespace = model)

    # Pass param_sim to overrides and return model, plotcomps, and param_sim
    model, param_sim = standard_options.overrides(param_sim, model)

    # Any additional kwarg handling for kwargs not in param_sim can be added here:
    # Setup logging level
    log = setupLogging(model, level = param_sim.logging_level)

    # Set fname to default if is None in param_sim:
    if param_sim.fname is None:
        param_sim.fname = (model.param_stim.Stimulation.Paradigm.name + '_' +
                           #model.param_stim.location.stim_dendrites[0])
                           #change to (after checking with Dan)
                           model.param_stim.Stimulation.StimLoc.stim_dendrites[0])

    ######### Add any new code here to parse additional possible kwargs.
    ######### Be sure to pop any parsed kwarg from kwargs dictionary.

    ######### Now kwargs should be empty, if not we passed an invalid kwarg
    if len(kwargs) > 0:
        log.warning("Passed invalid keyword arguments {} to setupOptions",
                    kwargs)
    return model # Not necessary to return


#@util.call_counter
def setupNeurons(model, **kwargs):
    '''Creates neuron(s) defined by model.

    By default, uses param_sim imported with model (model.param_sim), but
    passing 'param_sim=param_sim' in as a kwarg allows overriding; when called
    in ajustador, the param_sim defined by ajustador is explicitly passed in.'''

    if hasattr(model,'neurons'):
        model.log.warning('Neurons already setup. Returning.')
        return

    # If network (expects Boolean) passed to setupNeurons, get the value. otherwise set to None
    if 'network' in kwargs:
        network = kwargs.pop('network')
    else:
        network = None

    if 'param_sim' in kwargs:
        param_sim = kwargs['param_sim']
    else:
        param_sim = model.param_sim
    if getattr(param_sim, 'neuron_type', None) is not None:
        model.param_cond.neurontypes = util.neurontypes(model.param_cond,
                                                    [param_sim.neuron_type])
    # build neurons and specify returns to model namespace
    model.syn, model.neurons = cell_proto.neuronclasses(model)
    # If calcium and synapses created, could test plasticity at a single synapse
    # in syncomp. Need to debug this since eliminated param_sim.stimtimes. See
    # what else needs to be changed in plasticity_test.
    model.plas = {}
    #if model.plasYN:
    #    model.plas, model.stimtab = plasticity_test.plasticity_test(model)
                                                    #param_sim.syncomp,
                                                    #model.syn,
                                                    #param_sim.stimtimes)

    ########## clocks are critical. assign_clocks also sets up the hsolver

    if not network:
        print("Not simulating network; setting up simpaths and clocks in create_model_sim")
        simpaths=['/'+neurotype for neurotype in util.neurontypes(model.param_cond)]
        clocks.assign_clocks(simpaths, param_sim.simdt, param_sim.plotdt,
                             param_sim.hsolve, model.param_cond.NAME_SOMA)
        # Fix calculation of B parameter in CaConc if using hsolve
        if model.param_sim.hsolve and model.calYN:
            calcium.fix_calcium(util.neurontypes(model.param_cond), model)
    else:
        print("Simulating network; not setting up simpaths and clocks in create_model_sim")

    print('****Model.plasYN = {}*****'.format(model.plasYN))

    return model


def setupStim(model,**kwargs):
    '''Setup the stimulation pulse generator. This function requires that the
    neurons and options have already been setup'''
    if getattr(model,'inject_pop',False):
        neuron_paths = model.inject_pop
    else:
        neuron_paths = {ntype:[neur.path] for ntype, neur in model.neurons.items()}
    pg, param_sim = inject_func.setup_stim(model, model.param_sim, neuron_paths)
    model.pg, model.param_sim = pg, param_sim
    return model

def setupOutput(model, **kwargs):
    ###############--------------output elements
    (vmtab,
     catab,
     plastab,
     currtab) = tables.graphtables(model, model.neurons,
                                   model.param_sim.plot_current,
                                   model.param_sim.plot_current_message,
                                   model.plas,
                                   model.param_sim.plotcomps)

    if model.param_sim.save:
        writer=tables.setup_hdf5_output(model, model.neurons,
                                        filename=model.param_sim.fname,
                                        compartments=model.param_sim.plotcomps)
    else:
        writer=None
    model.writer = writer
    # Add tables to model namespace to simplify passing/returning
    model.vmtab = vmtab
    model.catab = catab
    model.plastab = plastab
    model.currtab = currtab

    if model.log.logger.getEffectiveLevel() == logging.DEBUG:
        for neur in model.neurons.keys():
            print_params.print_elem_params(model, neur, model.param_sim)

    if model.param_sim.plot_channels:
        plt.ion()
        if type(model.param_sim.plot_channels) is str:
            useChans = [model.param_sim.plot_channels] #Convert to list of len 1
        elif type(model.param_sim.plot_channels) is list:
            useChans = model.param_sim.plot_channels # Use the list of channel name strings
        else:
            useChans = model.Channels.keys() # Use all channels
        for chan in useChans:
            libchan = moose.element('/library/'+chan)
            plot_channel.plot_gate_params(libchan,
                                          model.param_sim.plot_activation,
                                          model.VMIN, model.VMAX,
                                          model.CAMIN, model.CAMAX)

    if model.spineYN:
        model.spinecatab, model.spinevmtab = tables.spinetabs(model,
                                                              model.neurons,
                                                              #model.param_sim.plotcomps,
                                                              )
    else:
        model.spinevmtab = []

    model.spiketab=tables.spiketables(model.neurons, model.param_cond)

    if getattr(model.param_sim,'plotgate',None):
        plotgate = model.param_sim.plotgate
        gatepath = list(model.neurons.values())[0].path+'/'+model.NAME_SOMA+'/'+plotgate
        gate = moose.element(gatepath)
        model.gatetables = {}
        gatextab=moose.Table('/data/gatex')
        moose.connect(gatextab, 'requestOut', gate, 'getX')
        model.gatetables['gatextab']=gatextab
        gateytab=moose.Table('/data/gatey')
        moose.connect(gateytab, 'requestOut', gate, 'getY')
        model.gatetables['gateytab']=gateytab
        if model.Channels[plotgate][0][2]==1:
            gateztab=moose.Table('/data/gatez')
            moose.connect(gateztab, 'requestOut', gate, 'getZ')
            model.gatetables['gateztab']=gateztab
    return


def runOneSim(model, simtime=None, injection_current=None):
    if model.param_stim.Stimulation.Paradigm.name == 'inject':
        print(u'◢◤◢◤◢◤◢◤ injection_current = {} ◢◤◢◤◢◤◢◤'.format(injection_current))
        model.pg.firstLevel = injection_current
    if simtime is None: simtime = model.param_sim.simtime
    moose.reinit()
    moose.start(simtime)

def stepRunPlot(model, **kwargs):
    if 'neuron' in kwargs:
        mod = kwargs['neuron']
    else:
        mod = list(model.neurons.values())[0][0]
    mod.buildSegmentTree()
    import moogul
    mv = moogul.MooView()
    #mv.ax.set_proj_type('ortho')

    # [baseclass, fieldGetFunc, scale, axisText, min, max]
    fieldinfo = ['Compartment', 'getVm', 1, 'Vm', 0, 1]
    m = moogul.MooNeuron(mod,fieldinfo, maxLineWidth = 10, diaScale = 10e6, colormap = 'viridis')
    mv.addDrawable(m)
    mv.firstDraw()
    #mv.ax.set_proj_type('ortho')

    plt.pause(1)
    mv.updateValues()
    plt.pause(60)

    #steps = int(np.round(model.param_sim.simtime/(model.param_sim.plotdt)))
    #for i in range(steps):
    #    moose.start(model.param_sim.plotdt)
    #    mv.updateValues()
    #    plt.pause(.01)

def runAll(model, plotIndividualInjections=False, writeWavesCSV=False, printParams = False):
    plt.ion()
    if model.plasYN:
        plotIndividualInjections=True
    traces, names, catraces, current_traces, curr_names = [], [], [], [], []
    for inj in model.param_sim.injection_current:
        runOneSim(model, simtime=model.param_sim.simtime, injection_current=inj)
        if model.param_sim.plot_vm and plotIndividualInjections:
            neuron_graph.graphs(model, model.vmtab, model.param_sim.plot_current,
                                model.param_sim.simtime, model.currtab,
                                model.param_sim.plot_current_label,
                                model.catab, model.plastab)
        #set up tables that accumulate soma traces for multiple simulations
        for neurnum,neurtype in enumerate(model.neurons.keys()):
            for plotcompnum, plotcomp in enumerate(model.param_sim.plotcomps):
                traces.append(model.vmtab[neurtype][plotcompnum].vector)
                if model.calYN and model.param_sim.plot_calcium:
                    catraces.append(model.catab[neurtype][plotcompnum].vector)
                names.append('{} {} @ {}'.format(plotcomp, neurtype, inj))
                # In Python3.6, the following syntax works:
                #names.append(f'{neurtype} @ {inj}')
        if model.param_sim.plot_current:
            for channame in model.Channels.keys():
                current_traces.append(model.currtab[neurtype][channame][0].vector)
                curr_names.append('{}: {} @ {}'.format(neurtype, channame,inj))

        #plot spines
        if len(model.spinevmtab) and model.param_sim.plot_vm:
            spine_graph.spineFig(model, model.spinecatab, model.spinevmtab,
                                 model.param_sim.simtime)
        #save plain text output - expand this to optionally save current data
        if model.param_sim.save_txt:
            tables.write_textfiles(model, inj, ca=False, spines=False, spineca=False)

        # Switch hdf5writer mode from 2 (overwrite) to 1 (append)
        # Note that hdf5writer is initialized in mode 2, overwriting prior simulations,
        # But within one simulation at multiple current injections setting mode to 1
        # allows appending the file with each current injection iteration,
        # and calling "wrap_hdf5" modifies the hdf5 file for each injection
        if model.param_sim.save:
            model.writer.mode=1
            model.writer.close()
            tables.wrap_hdf5(model,'injection_{}'.format(inj))

    if model.param_sim.plot_vm:
        neuron_graph.SingleGraphSet(traces, names, model.param_sim.simtime)
        if model.calYN and model.param_sim.plot_calcium:
            neuron_graph.SingleGraphSet(catraces, names, model.param_sim.simtime,title='Calcium')

    if model.param_sim.plot_current:
        num_currents=np.shape(current_traces)[0]//len(model.param_sim.injection_current)
        neuron_graph.SingleGraphSet(current_traces[-num_currents:], curr_names,model.param_sim.simtime)
        if getattr(model.param_sim,'plotgate',None):
            plt.figure()
            ts = np.linspace(0, model.param_sim.simtime, len(model.gatetables['gatextab'].vector))
            plt.suptitle('X,Y,Z gates; hsolve='+str(model.param_sim.hsolve)+' calYN='+str(model.calYN)+' Zgate='+str(model.Channels[model.param_sim.plotgate][0][2]))
            plt.plot(ts,model.gatetables['gatextab'].vector,label='X')
            plt.plot(ts,model.gatetables['gateytab'].vector,label='Y')
            if model.Channels[model.param_sim.plotgate][0][2]==1:
                plt.plot(ts,model.gatetables['gateztab'].vector,label='Z')
            plt.legend()

    util.block_if_noninteractive()
    for st in model.spiketab:
          print("number of spikes", st.path, ' = ',len(st.vector))

    model.traces, model.catraces = traces, catraces
    if model.param_sim.save:
        tables.save_hdf5_attributes(model)
        model.writer.close()
    if writeWavesCSV:
        timeCol = np.linspace(0, model.param_sim.simtime, len(model.traces[0]))*1e3 #ms
        vCol = model.traces[0] *1e3 #mV
        inj = model.param_sim.injection_current[0]
        header = 'Time (ms),{} pA'.format(inj*1e12)
        np.savetxt(model.param_sim.fname+'difshellwaves.csv', np.column_stack((timeCol,vCol)), delimiter=',', header = header, comments='' )


def setupAll(model,**kwargs):
    setupOptions(model, **kwargs)
    setupNeurons(model)
    setupOutput(model)
    setupStim(model)
    return model
