"""
Python rewrite of NEURON extracellular stimulation code
Replaces the HOC workflow for loading SWC, applying potentials, and detecting firing
"""

import numpy as np
from neuron import h
from neuron.units import ms, mV
import sys

# Load standard files
h.load_file("stdrun.hoc")
h.load_file("import3d.hoc")

class NeuronModel:
    def __init__(self, swc_file="pruned.swc"):
        """Initialize neuron model from SWC file"""
        self.swc_file = swc_file
        self.sections = []
        self.fired = False
        self.detector = None
        
        # Import morphology
        self._import_swc()
        
        # Set parameters
        self._set_parameters()
        
        # Calculate segment coordinates
        self._calculate_segment_coords()
        
    def _import_swc(self):
        """Load topology from SWC file"""
        cell = h.Import3d_SWC_read()
        cell.input(self.swc_file)
        i3d = h.Import3d_GUI(cell, 0)
        i3d.instantiate(None)
        
        # Store sections
        self.sections = [sec for sec in h.allsec()]
        print(f"Loaded {len(self.sections)} sections from {self.swc_file}")
        
    def _set_parameters(self):
        """Set up geometry and membrane parameters"""
        # Global parameters for all sections
        for sec in h.allsec():
            sec.Ra = 100
            sec.insert('pas')
            sec.g_pas = 3e-5
            sec.e_pas = -75
            sec.insert('extracellular')
            sec.insert('xtra')
        
        # Soma-specific
        for sec in h.allsec():
            if 'soma' in sec.name():
                sec.cm = 1
                sec.insert('CaDynamics_E2_soma')
                sec.insert('Ca_HVA_soma')
                sec.insert('Ca_LVAst_soma')
                sec.insert('Ih')
                sec.insert('NaTs2_t_soma')
                sec.insert('SK_E2_soma')
                sec.insert('SKv3_1_soma')
        
        # Axon-specific
        for sec in h.allsec():
            if 'axon' in sec.name():
                sec.cm = 1
                sec.insert('CaDynamics_E2')
                sec.insert('Ca_HVA')
                sec.insert('Ca_LVAst')
                sec.insert('K_Pst')
                sec.insert('K_Tst')
                sec.insert('NaTa_t')
                sec.insert('Nap_Et2')
                sec.insert('SK_E2')
                sec.insert('SKv3_1')
        
        # Dendrite-specific
        for sec in h.allsec():
            if 'dend' in sec.name():
                sec.cm = 2
                sec.insert('Ih')
        
        # Apical dendrite-specific
        for sec in h.allsec():
            if 'apic' in sec.name():
                sec.cm = 2
                sec.insert('Ih')
                sec.insert('Im')
                sec.insert('NaTs2_t_apic')
                sec.insert('SKv3_1_apic')
        
        # Set nseg based on AC length constant
        freq = 100  # Hz
        d_lambda = 0.01
        
        for sec in h.allsec():
            sec.nseg = int((sec.L / (d_lambda * self._lambda_f(sec, freq)) + 0.999) / 2) * 2 + 1
    
    def _lambda_f(self, sec, freq):
        """Calculate AC length constant for a section"""
        # Simplified lambda_f calculation
        return 1e5 * np.sqrt(sec.diam / (4 * np.pi * freq * sec.Ra * sec.cm))
    
    def _calculate_segment_coords(self):
        """Compute xyz coords of segments (interpxyz.hoc equivalent)"""
        for sec in h.allsec():
            if not hasattr(sec, 'n3d') or sec.n3d() == 0:
                continue
                
            # Get 3D point data
            n = sec.n3d()
            xx = np.array([sec.x3d(i) for i in range(n)])
            yy = np.array([sec.y3d(i) for i in range(n)])
            zz = np.array([sec.z3d(i) for i in range(n)])
            length = np.array([sec.arc3d(i) for i in range(n)])
            
            # Normalize length
            if length[-1] > 0:
                length_norm = length / length[-1]
            else:
                continue
            
            # Create range for segments
            nseg = sec.nseg
            range_vals = np.linspace(0, 1, nseg + 2)
            range_vals = range_vals - 1/(2*nseg)
            range_vals[0] = 0
            range_vals[-1] = 1
            
            # Interpolate coordinates
            x_interp = np.interp(range_vals, length_norm, xx)
            y_interp = np.interp(range_vals, length_norm, yy)
            z_interp = np.interp(range_vals, length_norm, zz)
            
            # Assign to segments (skip endpoints 0 and 1)
            for i, seg in enumerate(sec, start=1):
                seg.x_xtra = x_interp[i]
                seg.y_xtra = y_interp[i]
                seg.z_xtra = z_interp[i]
    
    def _set_pointers(self):
        """Link extracellular potential to xtra mechanism"""
        for sec in h.allsec():
            if hasattr(sec, 'ismembrane'):
                for seg in sec:
                    h.setpointer(seg._ref_e_extracellular, 'ex', seg.xtra)
    
    def load_potentials_from_file(self, potential_file):
        """
        Load extracellular potentials from file.
        File format: x y z potential (one per line)
        """
        print(f"Loading potentials from {potential_file}")
        
        try:
            data = np.loadtxt(potential_file)
            coords = data[:, :3]
            potentials = data[:, 3]
            
            # For each point in the file, find closest segment and assign potential
            for x, y, z, v in data:
                min_dist = float('inf')
                closest_sec = None
                closest_x = 0.5
                
                for sec in h.allsec():
                    if sec.n3d() == 0:
                        continue
                    
                    for i in range(sec.n3d()):
                        dx = x - sec.x3d(i)
                        dy = y - sec.y3d(i)
                        dz = z - sec.z3d(i)
                        dist = np.sqrt(dx*dx + dy*dy + dz*dz)
                        
                        if dist < min_dist:
                            min_dist = dist
                            closest_sec = sec
                            if sec.L > 0:
                                closest_x = sec.arc3d(i) / sec.L
                            else:
                                closest_x = 0.5
                
                if closest_sec is not None:
                    closest_sec(closest_x).e_extracellular = v
            
            print(f"Loaded {len(data)} potential values")
            
        except Exception as e:
            print(f"Error loading potential file: {e}")
            sys.exit(1)
    
    def load_potentials_ordered(self, potential_file):
        """
        Load potentials from file where order matches segment order.
        File format: one potential value per line
        """
        print(f"Loading ordered potentials from {potential_file}")
        
        try:
            potentials = np.loadtxt(potential_file)
            
            idx = 0
            for sec in h.allsec():
                for seg in sec:
                    if idx < len(potentials):
                        seg.es_xtra = potentials[idx]
                        idx += 1
            
            print(f"Loaded {idx} potential values")
            
        except Exception as e:
            print(f"Error loading potential file: {e}")
            sys.exit(1)
    
    def setup_stimulus(self, delay=0, duration=0.1, scale=-70):
        """
        Set up extracellular stimulus waveform.
        
        Parameters:
        -----------
        delay : float
            Stimulus delay in ms
        duration : float
            Stimulus duration in ms
        scale : float
            Stimulus scaling factor (unitless)
        """
        # Create stimulus vectors
        stim_scale = h.Vector([0, 0, 1, 1, 0, 0])
        stim_scale.mul(scale)
        
        stim_time = h.Vector([0, delay, delay, delay+duration, 
                              delay+duration, delay+duration+1])
        
        # Attach to xtra mechanism (only need to do once since stim_xtra is GLOBAL)
        for sec in h.allsec():
            if hasattr(sec(0.5), 'xtra'):
                stim_scale.play(sec(0.5).xtra._ref_stim_xtra, stim_time, 1)
                break
        
        print(f"Stimulus: delay={delay}ms, duration={duration}ms, scale={scale}")
    
    def setup_detector(self, section_name='axon', threshold=0):
        """Set up spike detector"""
        self.fired = False
        
        # Find the target section
        target_sec = None
        for sec in h.allsec():
            if section_name in sec.name():
                target_sec = sec
                break
        
        if target_sec is None:
            print(f"Warning: Could not find section matching '{section_name}'")
            target_sec = self.sections[0]
        
        # Create detector
        self.detector = h.NetCon(target_sec(0.5)._ref_v, None, sec=target_sec)
        self.detector.threshold = threshold
        self.detector.record(self._handle_spike)
    
    def _handle_spike(self):
        """Callback for spike detection"""
        self.fired = True
        h.stoprun = 1
    
    def run_simulation(self, tstop=10):
        """Run simulation"""
        self.fired = False
        h.finitialize(-65 * mV)
        h.continuerun(tstop * ms)
        return self.fired
    
    def find_threshold(self, delay=0, duration=0.1, output_file="threshold_results.txt"):
        """
        Binary search to find threshold stimulus amplitude.
        
        Parameters:
        -----------
        delay : float
            Stimulus delay in ms
        duration : float  
            Stimulus duration in ms
        output_file : str
            File to save results
        """
        print("Finding threshold...")
        
        # Initialize detector
        self.setup_detector('axon')
        
        # Binary search parameters
        sca = -20
        sca_top = sca
        sca_bottom = sca
        first_fired = False
        first_unfired = False
        max_iter = 100
        tolerance = 1e-2
        
        results = []
        
        for iteration in range(1, max_iter + 1):
            if abs(sca_top - sca_bottom) < tolerance and iteration > 1:
                break
            
            # Set stimulus and run
            self.setup_stimulus(delay, duration, sca)
            fired = self.run_simulation()
            
            result = f"Iteration {iteration}: SCA = {sca:.6f}, fired = {int(fired)}"
            print(result)
            results.append(result)
            
            # Update search bounds
            if fired and not first_unfired:
                sca_top = sca
                sca = sca / 2
                sca_bottom = sca
                first_fired = True
            elif fired and first_unfired:
                sca_top = sca
                sca = (sca_top + sca_bottom) / 2
                first_fired = True
            elif not fired and not first_fired:
                sca_bottom = sca
                sca = sca * 2
                sca_top = sca
                first_unfired = True
            elif not fired and first_fired:
                sca_bottom = sca
                sca = (sca_top + sca_bottom) / 2
                first_unfired = True
        
        # Save results
        with open(output_file, 'w') as f:
            for result in results:
                f.write(result + '\n')
        
        print(f"Threshold: {sca:.6f}")
        print(f"Results saved to {output_file}")
        return sca
    
    def export_segment_locations(self, output_file="locs_all_seg.txt"):
        """Export segment positions and parent connections"""
        with open(output_file, 'w') as f:
            for sec in h.allsec():
                parent_sec = sec.parentseg()
                has_parent = parent_sec is not None
                
                for seg in sec:
                    x_coord = seg.x_xtra
                    y_coord = seg.y_xtra
                    z_coord = seg.z_xtra
                    
                    # Determine parent coordinates
                    if seg == list(sec)[0] and has_parent:
                        # First segment - parent is last segment of parent section
                        px = parent_sec.x_xtra
                        py = parent_sec.y_xtra
                        pz = parent_sec.z_xtra
                    elif seg == list(sec)[0] and not has_parent:
                        # Root section first segment
                        px = py = pz = float('nan')
                    else:
                        # Parent is previous segment in same section
                        prev_seg = list(sec)[list(sec).index(seg) - 1]
                        px = prev_seg.x_xtra
                        py = prev_seg.y_xtra
                        pz = prev_seg.z_xtra
                    
                    f.write(f"{x_coord:.4f} {y_coord:.4f} {z_coord:.4f} "
                           f"{px:.4f} {py:.4f} {pz:.4f}\n")
        
        print(f"Segment locations exported to {output_file}")


def main():
    """Main execution function"""
    # Configuration
    SWC_FILE = "pruned.swc"
    #POTENTIAL_FILE = "pruned.ptntl"  # x y z potential format
    POTENTIAL_TXT = "pruned.txt"  # ordered potential values
    
    DELAY = 0      # ms
    DURATION = 0.1 # ms
    SCALE = -70    # unitless
    
    OUTPUT_FILE = "threshold_results.txt"
    
    # Create model
    model = NeuronModel(swc_file=SWC_FILE)
    
    # Load potentials (choose one method)
    #model.load_potentials_from_file(POTENTIAL_FILE)
    model.load_potentials_ordered(POTENTIAL_TXT)
    
    # Export segment locations
    model.export_segment_locations()
    
    # Find threshold
    threshold = model.find_threshold(DELAY, DURATION, OUTPUT_FILE)
    
    print(f"\nFinal threshold: {threshold:.6f}")


if __name__ == "__main__":
    main()

