from __future__ import print_function
from neuron import h
import numpy as np
import scipy
import scipy.integrate
import scipy.stats
import scipy.optimize

try:
    import pyqtgraph as pg

    HAVE_PG = True
except ImportError:
    HAVE_PG = False

from ..util.stim import make_pulse
from ..util import fitting
from ..util import custom_init
from .protocol import Protocol


class IVCurve(Protocol):
    def __init__(self):
        super(IVCurve, self).__init__()

    def reset(self):
        super(IVCurve, self).reset()
        self.voltage_traces = []
        self.durs = None  # durations of current steps
        self.current_cmd = None  # Current command levels
        self.current_traces = []
        self.time_values = None
        self.dt = None
        self.initdelay = 0.0

    def run(
        self,
        ivrange,
        cell,
        durs=None,
        sites=None,
        reppulse=None,
        temp=22,
        dt=0.025,
        initdelay=0.0,
    ):
        """
        Run a current-clamp I/V curve on *cell*.
        
        Parameters
        ----------
        ivrange : dict of list of tuples
            Each item in the list is (min, max, step) describing a range of 
            levels to test. Range values are inclusive, so the max value may
            appear in the test values. Using multiple ranges allows finer 
            measurements in some ranges.
            For example::
            
                {'pulse': [(-1., 0., 1.), (-0.1, 0., 0.02)], 'prepulse': [(-0.5, 0, 0.1)]}
         
                Optional keys include 'pulsedur' : the duration of the pulse, in ms
                                      'prepulsecur: the duration of the prepulse, in ms
                The prepulse or the pulse can have a single value if the other is ranged.
        cell : Cell
            The Cell instance to test.
        durs : tuple
            durations of (pre, pulse, post) regions of the command
        sites : list
            Sections to add recording electrodes
        reppulse : 
            stimulate with pulse train
        temp : 
            temperature of simulation (32)
        dt : 
            timestep of simulation (0.025)
            
        """
        self.reset()
        self.cell = cell
        self.initdelay = initdelay
        self.dt = dt
        self.temp = temp
        # Calculate current pulse levels
        icur = []
        precur = [0.0]
        self.pre_current_cmd = []
        npresteps = 0
        if isinstance(ivrange["pulse"], tuple):
            icmd = [ivrange["pulse"]]  # convert to a list with tuple(s) embedded
        else:
            icmd = ivrange["pulse"]  # was already a list with multiple tuples
        for c in icmd:  # unpack current levels for the main pulse
            try:
                (imin, imax, istep) = c  # unpack a tuple... or list
            except:
                raise TypeError(
                    "run_iv arguments must be a dict with tuples {'pulse': (imin, imax, istep), 'prepulse': ...}"
                )
            nstep = np.floor((imax - imin) / istep) + 1
            icur.extend(imin + istep * np.arange(nstep))  # build the list
        self.current_cmd = np.array(sorted(icur))
        nsteps = self.current_cmd.shape[0]
        # Configure IClamp
        if durs is None:
            durs = [10.0, 100.0, 50.0]  # set default durs

        if "prepulse" in ivrange.keys():
            if isinstance(ivrange["prepulse"], tuple):
                icmd = [ivrange["prepulse"]]  # convert to a list with tuple(s) embedded
            else:
                icmd = ivrange["prepulse"]  # was already a list with multiple tuples
            precur = []
            for c in icmd:
                try:
                    (imin, imax, istep) = c  # unpack a tuple... or list
                except:
                    raise TypeError(
                        "run_iv arguments must be a dict with tuples {'pulse': (imin, imax, istep), 'prepulse': ...}"
                    )
                nstep = np.floor((imax - imin) / istep) + 1
                precur.extend(imin + istep * np.arange(nstep))  # build the list
            self.pre_current_cmd = np.array(sorted(precur))
            npresteps = self.pre_current_cmd.shape[0]
            durs.insert(1, 50.0)

        self.durs = durs
        # set up stimulation with a pulse train
        if reppulse is not None:
            stim = {
                "NP": 10,
                "Sfreq": 50.0,
                "delay": 10.0,
                "dur": 2,
                "amp": 1.0,
                "PT": 0.0,
                "dt": self.dt,
            }
        elif "prepulse" in ivrange.keys():
            stim = {
                "NP": 2,
                "delay": durs[0],
                "predur": durs[1],
                "dur": durs[2],
                "amp": 1.0,
                "preamp": 0.0,
                "dt": self.dt,
            }
            self.p_start = durs[0] + durs[1]
            self.p_end = self.p_start + durs[2]
            self.p_dur = durs[2]
        else:
            stim = {
                "NP": 1,
                "delay": durs[0],
                "dur": durs[1],
                "amp": 1.0,
                "dt": self.dt,
            }
            self.p_start = durs[0]
            self.p_end = self.p_start + durs[1]
            self.p_dur = durs[1]
        # print stim
        # print('p_: ', self.p_start, self.p_end, self.p_dur)
        istim = h.iStim(0.5, sec=cell.soma)
        istim.delay = 5.0
        istim.dur = 1e9  # these actually do not matter...
        istim.iMax = 0.0
        self.tend = np.sum(durs)  # maxt + len(iextend)*stim['dt']

        self.cell = cell
        for i in range(nsteps):
            # Generate current command for this level
            stim["amp"] = self.current_cmd[i]
            if npresteps > 0:
                for j in range(npresteps):
                    stim["preamp"] = self.pre_current_cmd[j]
                    self.run_one(istim, stim, initflag=(i == 0 and j == 0))
            else:
                self.run_one(istim, stim, initflag=(i == 0))

    def run_one(self, istim, stim, initflag=True):
        """
        Perform one run in current-clamp for the selected cell
        and add the data to the traces
        
        Parameters
        ----------
        istim : Stimulus electrode instance
        stim : waveform information
        initflag : boolean (default: True)
            If true, force initialziation of the cell and computation of 
            point Rin, tau and Vm
        """
        (secmd, maxt, tstims) = make_pulse(stim)
        # print('maxt, dt*lencmd: ', maxt, len(secmd)*self.dt)# secmd = np.append(secmd, [0.])
        # print('stim: ', stim, self.tend)

        # connect current command vector
        playvector = h.Vector(secmd)
        playvector.play(istim._ref_i, h.dt, 0, sec=self.cell.soma)

        # Connect recording vectors
        self["v_soma"] = self.cell.soma(0.5)._ref_v
        # self['q10'] = self.cell.soma(0.5).ihpyr_adj._ref_q10
        # self['ih_ntau'] = self.cell.soma(0.5).ihpyr_adj._ref_kh_n_tau
        self["i_inj"] = istim._ref_i
        self["time"] = h._ref_t

        # h('secondorder=0')  # direct call fails; let hoc do the work
        h.celsius = self.cell.status["temperature"]
        self.cell.cell_initialize()
        h.dt = self.dt
        custom_init(v_init=self.cell.vm0)

        h.t = 0.0
        h.tstop = self.tend
        while h.t < h.tstop:
            h.fadvance()

        self.voltage_traces.append(self["v_soma"])
        self.current_traces.append(self["i_inj"])
        self.time_values = np.array(self["time"] - self.initdelay)
        # self.mon_q10 = np.array(self['q10'])
        # self.mon_ih_ntau = np.array(self['ih_ntau'])

    def peak_vm(self, window=0.5):
        """
        Parameters
        ----------
        window : float (default: 0.5)
            fraction of trace to look at to find peak value
        
        Returns
        -------
        peak membrane voltage for each trace.

        """
        Vm = self.voltage_traces
        Icmd = self.current_cmd
        steps = len(Icmd)
        peakStart = int(self.p_start / self.dt)
        peakStop = int(
            peakStart + (self.p_dur * window) / self.dt
        )  # peak can be in first half
        Vpeak = []
        for i in range(steps):
            if Icmd[i] > 0:
                Vpeak.append(Vm[i][peakStart:peakStop].max())
            else:
                Vpeak.append(Vm[i][peakStart:peakStop].min())
        return np.array(Vpeak)

    def steady_vm(self, window=0.1):
        """
        Parameters
        ----------
        window: (float) default: 0.1
        fraction of window to use for steady-state measurement, taken
        immediately before the end of the step
        
        Returns
        -------
        steady-state membrane voltage for each trace.
        
        """
        Vm = self.voltage_traces
        steps = len(Vm)
        steadyStop = int((self.p_end) / self.dt)
        steadyStart = int(
            steadyStop - (self.p_end * window) / self.dt
        )  # measure last 10% of trace
        Vsteady = [Vm[i][steadyStart:steadyStop].mean() for i in range(steps)]
        return np.array(Vsteady)

    def spike_times(self, threshold=None):
        """
        Return an array of spike times for each trace.
        
        Parameters
        ----------
        threshold: float (default: None)
            Optional threshold at which to detect spikes. By 
            default, this queries cell.spike_threshold.
        
        Returns
        -------
        list of spike times.
        
        """
        if threshold is None:
            threshold = self.cell.spike_threshold

        Vm = self.voltage_traces
        steps = len(Vm)
        spikes = []
        for i in range(steps):
            # dvdt = np.diff(Vm[i]) / self.dt
            # mask = (dvdt > 40).astype(int)
            mask = (Vm[i] > threshold).astype(int)
            indexes = np.argwhere(np.diff(mask) == 1)[:, 0] + 1
            times = indexes.astype(float) * self.dt
            spikes.append(times)
        return spikes

    def spike_filter(self, spikes, window=(0.0, np.inf)):
        """Filter the spikes to only those occurring in a defined window.
        
        Required to compute input resistance in traces with no spikes during
        the stimulus, because some traces will have anodal break spikes.
        
        Parameters
        ----------
        spikes : list
            the list of spike trains returned from the spike_times method
        window : (start, stop)
            the window over which to look for spikes (in msec: default is
            the entire trace).

        Returns
        -------
        the spikes in a list
        
        """
        filteredspikes = []
        for i in range(len(spikes)):
            winspikes = []  # spikes is arranged by current; so this is for one level
            for j in range(len(spikes[i])):
                if spikes[i][j] >= window[0] and spikes[i][j] <= window[1]:
                    winspikes.append(spikes[i][j])
            filteredspikes.append(winspikes)  # now build filtered spike list
        return filteredspikes

    def rest_vm(self):
        """
        Parameters
        ----------
        None
        
        Returns
        -------
        The mean resting membrane potential.
        """
        d = int(self.durs[0] / self.dt)
        rvm = np.array(
            [
                np.array(self.voltage_traces[i][d // 2 : d]).mean()
                for i in range(len(self.voltage_traces))
            ]
        ).mean()
        return rvm

    def input_resistance_tau(self, vmin=-10.0, imax=0, return_fits=False):
        """
        Estimate resting input resistance and time constant.
        
        Parameters
        ----------
        vmin : float
            minimum voltage to use in computation relative to resting
        imax : float
            maximum current to use in computation.
        return_eval : bool
            If True, return lmfit.ModelFit instances for the subthreshold trace
            fits as well.
            
        Returns
        -------
        dict :
            Dict containing:
            
            * 'slope' and 'intercept' keys giving linear 
              regression for subthreshold traces near rest
            * 'tau' giving the average first-exponential fit time constant
            * 'fits' giving a record array of exponential fit data to subthreshold
              traces.
        
        Analyzes only traces hyperpolarizing pulse traces near rest, with no 
        spikes.
        
        """
        Vss = self.steady_vm()
        vmin += self.rest_vm()
        Icmd = self.current_cmd
        rawspikes = self.spike_times()
        spikes = self.spike_filter(rawspikes, window=[self.p_start, self.p_end])
        steps = len(Icmd)

        nSpikes = np.array([len(s) for s in spikes])
        # find traces with Icmd < 0, Vm > -70, and no spikes.
        vmask = Vss >= vmin
        imask = Icmd <= imax
        smask = nSpikes > 0
        mask = vmask & imask & ~smask
        if mask.sum() < 2:
            print(
                "WARNING: Not enough traces to do linear regression in "
                "IVCurve.input_resistance_tau()."
            )
            print(
                "{0:<15s}: {1:s}".format(
                    "vss", ", ".join(["{:.2f}".format(v) for v in Vss])
                )
            )
            print(
                "{0:<15s}: {1:s}".format(
                    "Icmd", ", ".join(["{:.2f}".format(i) for i in Icmd])
                )
            )
            print("{0:<15s}: {1:s}".format("vmask", repr(vmask.astype(int))))
            print("{0:<15s}: {1:s} ".format("imask", repr(imask.astype(int))))
            print("{0:<15s}: {1:s}".format("spikemask", repr(smask.astype(int))))
            raise Exception(
                "Not enough traces to do linear regression (see info above)."
            )

        # Use these to measure input resistance by linear regression.
        reg = scipy.stats.linregress(Icmd[mask], Vss[mask])
        (slope, intercept, r, p, stderr) = reg

        # also measure the tau in the same traces:
        pulse_start = int(self.p_start / self.dt)
        pulse_stop = int((self.p_end) / self.dt)

        fits = []
        fit_inds = []
        tx = self.time_values[pulse_start:pulse_stop].copy()
        for i, m in enumerate(mask):
            if not m or (self.rest_vm() - Vss[i]) <= 1:
                continue
            trace = self.voltage_traces[i][pulse_start:pulse_stop]

            # find first minimum in the trace
            min_ind = np.argmin(trace)
            min_val = trace[min_ind]
            min_diff = trace[0] - min_val
            tau_est = min_ind * self.dt * (1.0 - 1.0 / np.e)
            # print ('minind: ', min_ind, tau_est)
            fit = fitting.Exp1().fit(
                trace[:min_ind],
                method="nelder",
                x=tx[:min_ind],
                xoffset=(tx[0], "fixed"),
                yoffset=(min_val, -120.0, -10.0),
                amp=(min_diff, 0.0, 50.0),
                tau=(tau_est, 0.5, 50.0),
            )

            # find first maximum in the trace (following with first minimum)
            max_ind = np.argmax(trace[min_ind:]) + min_ind
            max_val = trace[max_ind]
            max_diff = min_val - max_val
            tau2_est = max_ind * self.dt * (1.0 - 1.0 / np.e)
            amp1_est = fit.params["amp"].value
            tau1_est = fit.params["tau"].value
            amp2_est = fit.params["yoffset"] - max_val
            # print('tau1, tau2est: ', tau1_est, tau2_est)
            # fit up to first maximum with double exponential, using prior
            # fit as seed.
            fit = fitting.Exp2().fit(
                trace[:max_ind],
                method="nelder",
                x=tx[:max_ind],
                xoffset=(tx[0], "fixed"),
                yoffset=(max_val, -120.0, -10.0),
                amp1=(amp1_est, 0.0, 200.0),
                tau1=(tau1_est, 0.5, 50.0),
                amp2=(amp2_est, -200.0, -0.5),
                tau_ratio=(tau2_est / tau1_est, 2.0, 50.0),
                tau2="tau_ratio * tau1",
            )

            fits.append(fit)
            fit_inds.append(i)
        # convert fits to record array
        #        print len(fits) # fits[0].params
        if len(fits) > 0:
            key_order = sorted(
                fits[0].params
            )  # to ensure that unit tests remain stable
            dtype = [(k, float) for k in key_order] + [("index", int)]
            fit_data = np.empty(len(fits), dtype=dtype)
            for i, fit in enumerate(fits):
                for k, v in fit.params.items():
                    fit_data[i][k] = v.value
                fit_data[i]["index"] = fit_inds[i]

            if "tau" in fit_data.dtype.fields:
                tau = fit_data["tau"].mean()
            else:
                tau = fit_data["tau1"].mean()
        else:
            slope = 0.0
            intercept = 0.0
            tau = 0.0
            fit_data = []
        ret = {"slope": slope, "intercept": intercept, "tau": tau, "fits": fit_data}

        if return_fits:
            return ret, fits
        else:
            return ret

    def show(self, cell=None, rmponly=False):
        """
        Plot results from run_iv()
        
        Parameters
        ----------
        cell : cell object (default: None)
        
        """
        if not HAVE_PG:
            raise Exception("Requires pyqtgraph")

        #
        # Generate figure with subplots
        #
        app = pg.mkQApp()
        win = pg.GraphicsWindow(
            "%s  %s (%s)"
            % (cell.status["name"], cell.status["modelType"], cell.status["species"])
        )
        self.win = win
        win.resize(1000, 800)
        Vplot = win.addPlot(labels={"left": "Vm (mV)", "bottom": "Time (ms)"})
        rightGrid = win.addLayout(rowspan=2)
        win.nextRow()
        Iplot = win.addPlot(labels={"left": "Iinj (nA)", "bottom": "Time (ms)"})

        IVplot = rightGrid.addPlot(labels={"left": "Vm (mV)", "bottom": "Icmd (nA)"})
        IVplot.showGrid(x=True, y=True)
        rightGrid.nextRow()
        spikePlot = rightGrid.addPlot(
            labels={"left": "Iinj (nA)", "bottom": "Spike times (ms)"}
        )
        rightGrid.nextRow()
        FIplot = rightGrid.addPlot(
            labels={"left": "Spike count", "bottom": "Iinj (nA)"}
        )

        win.ci.layout.setRowStretchFactor(0, 10)
        win.ci.layout.setRowStretchFactor(1, 5)

        #
        # Plot simulation and analysis results
        #
        Vm = self.voltage_traces
        Iinj = self.current_traces
        Icmd = self.current_cmd
        t = self.time_values
        steps = len(Icmd)

        # plot I, V traces
        colors = [(i, steps * 3.0 / 2.0) for i in range(steps)]
        for i in range(steps):
            Vplot.plot(t, Vm[i], pen=colors[i])
            Iplot.plot(t, Iinj[i], pen=colors[i])

        if rmponly:
            return
        # I/V relationships
        IVplot.plot(
            Icmd,
            self.peak_vm(),
            symbol="o",
            symbolBrush=(50, 150, 50, 255),
            symbolSize=4.0,
        )
        IVplot.plot(Icmd, self.steady_vm(), symbol="s", symbolSize=4.0)

        # F/I relationship and raster plot
        spikes = self.spike_times()
        for i, times in enumerate(spikes):
            spikePlot.plot(
                x=times,
                y=[Icmd[i]] * len(times),
                pen=None,
                symbol="d",
                symbolBrush=colors[i],
                symbolSize=4.0,
            )
        FIplot.plot(x=Icmd, y=[len(s) for s in spikes], symbol="o", symbolSize=4.0)

        # Print Rm, Vrest
        rmtau, fits = self.input_resistance_tau(return_fits=True)
        s = rmtau["slope"]
        i = rmtau["intercept"]
        # tau1 = rmtau['fits']['tau1'].mean()
        # tau2 = rmtau['fits']['tau2'].mean()
        # print ("\nMembrane resistance (chord): {0:0.1f} MOhm  Taum1: {1:0.2f}  Taum2: {2:0.2f}".format(s, tau1, tau2))

        # Plot linear subthreshold I/V relationship
        ivals = np.array([Icmd.min(), Icmd.max()])
        vvals = s * ivals + i
        line = pg.QtGui.QGraphicsLineItem(ivals[0], vvals[0], ivals[1], vvals[1])
        line.setPen(pg.mkPen(255, 0, 0, 70))
        line.setZValue(-10)
        IVplot.addItem(line, ignoreBounds=True)

        # plot exponential fits
        for fit in fits:
            t = np.linspace(self.p_start, self.p_end, 1000)
            y = fit.eval(x=t)
            Vplot.plot(
                t, y, pen={"color": (100, 100, 0), "style": pg.QtCore.Qt.DashLine}
            )

            # plot initial guess
            # y = fit.eval(x=t, **fit.init_params.valuesdict())
            # Vplot.plot(t, y, pen={'color': 'b', 'style': pg.QtCore.Qt.DashLine})

        print("Resting membrane potential: %0.1f mV\n" % self.rest_vm())
