# encoding: utf-8
"""
model.view_model -- Functionality for Model-based GUIs

This is complementary to the AbstractModel class and not a subclass of it. 
Subclasses should inherit from both Model (or a Model subclass) and this class
to take advantage of the visualization capability:

class MyModelGUI(ViewModel, Model):
    pass

ViewModel -- Super class with useful functionality for real-time data views
ViewModelHandler -- Handler subclass that starts a wx.Timer for refresh

Copyright (c) 2008 Columbia University. All rights reserved.
"""

# Library imports
import wx, numpy, threading

# Traits imports
from enthought.traits.api import HasTraits, Property, Range, Trait, Instance, \
    Float, Bool, Button
from enthought.traits.ui.api import Handler


class ViewModel(HasTraits):

    """
    Visualization helper class for AbstractModel subclasses
    
    The ViewModel base class complements AbstractModel by providing capabilities 
    for visualizing data traces in real-time as a AbstractModel simulation 
    progresses.
    
    Subclasses must override:
    _update_plots() -- update data sources for plots
    
    Traits and methods for subclass use:
    trail_length -- length of time into the past for data trails
    refresh_rate -- rate (Hz) at which plots are refreshed
    _trail(key) -- get data trail for a time-series (or time for key='t')
    _program_flow -- Button trait for pause/unpause button
    _reset_model -- Button trait for a model reset button
    
    Subclasses should inherit from ViewModel and Model (in that order).
    """
    
    # Traits for Play/Pause and Reset buttons
    _program_flow = Button
    _reset_simulation = Button

    # Data trail length in seconds
    trail_length = Range(low=0.0, value=5.0, exclude_low=True)
    
    # Thread for running the simulation loop
    _thread_ = Instance(threading.Thread)
    _view_open_ = Bool(False)
    
    # wx.Timer for refreshing plots
    refresh_rate = Range(low=2, high=30, value=12)
    _timer_ = Instance(wx.Timer)
    _t = Float(-1)
    
    # Public methods
    
    def _update_plots(self):
        """Subclasses override this to update data sources"""
        raise NotImplementedError
    
    def _trails(self, *keys):
        """
        Get the data trails for time-series indicated by keys tuple:        
        args -- string names of a trait attributes being tracked; passing in 
                't' returns the current time slice.
        
        Returns the trail data as a tuple of rank-1 arrays.
        """
        return self.ts.data_slice(keys, -self.trail_length)
    
    # Private methods
    
    def _update_(self, event):
        """Update plot data as model advances its timestep"""
        if self.t != self._t:
            self._update_plots()
            self._t = self.t
    
    def _pause_changed(self, paused):
        """Overriding Model to call advance() in its own thread"""
        if paused:
            if self._timer_.IsRunning():
                self._timer_.Stop()
        elif not (self._thread_ is not None and self._thread_.isAlive()):
            self._thread_ = threading.Thread(target=self.advance)
            self._thread_.start()
            self._timer_.Start(1000/float(self.refresh_rate), wx.TIMER_CONTINUOUS)
    
    def _done_changed(self, finished):
        if finished:
            self.pause = True
    
    # Actions for pause and reset button presses

    def __program_flow_fired(self):
        self.pause = not self.pause
    
    def __reset_simulation_fired(self):
        self.done = True
        self.reset()
        

class ViewModelHandler(Handler):
    """Handler subclass that initiates a timer for a ViewModel subclass"""

    def init(self, info):
        """Start timer after window creation and store in model object"""
        obj = info.object
        obj._view_open_ = True
        frame = info.ui.owner.control
        timer_id = wx.NewId()
        timer = wx.Timer(frame, timer_id)
        frame.Bind(wx.EVT_TIMER, obj._update_, id=timer_id)
        obj._timer_ = timer
    
    def closed(self, info, is_ok):
        """Join simulation thread for OK if still running, otherwise cleanup"""
        obj = info.object
        obj._view_open_ = False
        if is_ok:
            if obj.pause and not obj.done:
                obj.growl = True
                obj.out('Simulation is still paused. Start by setting pause ' +
                    'attribute to False.', 'Reminder', 'info')
            elif obj._thread_ is not None and obj._thread_.isAlive():
                obj._thread_.join()
        else:
            obj.done = True