import brlapi, time, logging
import AEOutput, AEInput
from threading import Thread
from i18n import _
import AEOutput.Constants as Constants

MAX_KEY_CHORD = 3
__uie__ = dict(kind='device')


def _keyCodeToKeyName(code):
  '''
  Gets the name of the given key code by using the default key map to lookup
  its keysym value and then its localized name. If the code is not known, an
  empty string is returned.
  
  @param code: Hardware keycode
  @type code: integer
  @return: Name of the key
  @rtype: string
  '''
  for name, value in brlapi.__dict__.items():
      if code is value:
        return name
  return ''

class BrlAPIStyle(AEOutput.StyleFlyweight):
  '''
  Overrides the base L{AEOutput.Style.StyleFlyweight} class to provide style
  settings relative to the default style for this device.
  '''
  pass

  def getSettings(self):
    pass

class BrlAPIStyleDefault(AEOutput.StyleDefault):
  '''
  Overrides the base L{AEOutput.Style.StyleDefault} class, filling in 
  fields for supported style properties with their appropriate values.
  '''
  # Caret display styles
  CaretNone = 1
  CaretTwoBottom = 2
  CaretBottomRight = 3
  CaretAll = 4
  CaretStatusCells = 5  # not shown in settings dialog now
  
  MaxCaretDisplay = 5
  MinCaretDisplay = 1
  CaretStyle = CaretTwoBottom
  
  # Trunction/ellipsis variables
  EllipsisRight = False
  EllipsisLeft = False
  
  # braille display properties
  DisplayColumns = 0
  DisplayRows = 0 
  TotalCells = 0
  
  def getSettings(self):
    '''
    Gets configurable absolute settings affecting all output from this device.
    
    @return: Group of all configurable settings
    @rtype: L{AEState.Setting.Group}
    '''
    g = self._newGroup()
    g.newEnum('CaretStyle', _('Caret Display Style'), 
              {_('No Caret') : self.CaretNone,
               _('Two Bottom Pins') : self.CaretTwoBottom,
               _('Bottom Right Pins') : self.CaretBottomRight,
               _('All Pins') : self.CaretAll},
              _('Defines the caret representation.'))
              
    e = g.newGroup(_('Ellipsis'))
    e.newBool('EllipsisRight', _('Ellipsis right?'), 
              _('When set, ellipsis will appear when additional text is to the\
 right of the viewport.'))
    e.newBool('EllipsisLeft', _('Ellipsis left?'), 
            _('When set, ellipsis will appear when additional text is to the\
 left of the viewport.'))
                        
    return g


class BrlInputEventListener(Thread):
  '''
  Monitors brlapi for input events from braille device.
  '''
  def __init__(self, brldev):
    Thread.__init__(self)
    self.brldev = brldev
    self.runflag = True
    
  def run(self):
    brlconn = self.brldev.getbrlconnection()
    while self.runflag:
      try:
        key = brlconn.readKey(block=False)
      except brlapi.OperationError:
        #TODO: log error. careful about loop!
        #increase sleep time?
        pass
      if key is not None:
        self.brldev._onKeyClick(key)
      time.sleep(.1)

  def stop(self):
    self.runflag = False


class BrlAPIDevice(AEOutput.Braille, AEInput.AEInput):
  '''
  Braille output via Brlapi interface.
  '''
  USE_THREAD = False
  DEFAULT_STYLE = BrlAPIStyleDefault
  FLYWEIGHT_STYLE = BrlAPIStyle
  
  def init(self):
    '''
    Initializes the braille interface.
    
    @raise AEOutput.InitError: When the device can not be initialized
    '''
    #TODO: fix initialization. put in __init__ of this class
    AEInput.AEInput.__init__(self)
    
    self.brlconn = None
    self.inputmngr = None
    self.last_style = None
    self.commands = []
    self.caretpos = 0
    self.log = logging.getLogger('Device')
    
    try:
      self.brlconn = brlapi.Connection()  
      self.brlconn.enterTtyMode()
    except brlapi.ConnectionError:
      raise AEOutput.InitError
    
    # load key command bindings and put in __class__.__dict__
    self._loadKeyCmdConstants()
    
    # start input event manager
    self.inputmngr = BrlInputEventListener((self))
    self.inputmngr.start()
    
    # populate display size properties
    self.default_style.DisplayColumns = self.brlconn.displaysize[0]
    self.default_style.DisplayRows = self.brlconn.displaysize[1]
    self.default_style.TotalCells = self.brlconn.displaysize[0] * self.brlconn.displaysize[1]
   
  def _loadKeyCmdConstants(self):
    for name, value in brlapi.__dict__.items():
      if name.startswith('KEY_CMD'):
        setattr(self.__class__, name, value)
    
  def getbrlconnection(self):
    return self.brlconn
     
  # -------------    begin input related methods  -----------------------------
  def _onKeyClick(self, key):
    '''
    Handles a key click event by acting on the hardware key code in
    key. 
    
    @param key: a brlapi structure representing a key click event
    @type brlapi.key: key
    '''
    try:
      (command, argument, flags) = self.brlconn.expandKeyCode(key)  
    except brlapi.OperationError:
      self.log.warn(_('braille input failed'))
      return
    print ("Key %ld (%x %x %x)" % (key, command, argument, flags))
    
    gesture = AEInput.Gesture(self)
    gesture.addActionCode(command)

      # notify listeners of the gesture and its timestamp
    self._notifyInputListeners(gesture, time.time())
              
  def _notifyInputListeners(self, gesture, timestamp):
    '''
    Notifies registered listeners about a L{Gesture} seen on the input device. 
    Catches all exceptions from the callback and logs them.
    
    @param gesture: L{Gesture} to send to listeners
    @type gesture: L{Gesture}
    @param timestamp: Time at which at the gesture happened
    @type timestamp: float
    '''
    for i in gesture.action_codes:
      print "_notifyInputListeners: gesture actioncodes = %x" %i
      
    for listener in self.listeners:
      print "calling listener ", listener
      try:
        listener(gesture, timestamp)
      except Exception:
        print "got exception", Exception
        #log.exception('AEInput exception')
        
  def getCapabilities(self):
    '''
    Gets a list of strings representing the capabilities of this device. 
    Typical output capabilities include "system input," "braille," "switch,"
    etc. though others are certainly possible.
    
    The L{DeviceManager} will only load a device if another device doesn't
    already provide all of its capabilities.
    
    @return: Lowercase names of output capabilities
    @rtype: list of string
    '''
    return ['braille']
       
  def sortGesture(self, gesture):
    '''    
    Sort the provided L{Gesture} according to a sort order
    specific to the type of input device.
    
    @param gesture: L{Gesture} object to sort
    @type gesture: L{Gesture}
    @return: List of action codes sorted according to the specific device 
        implementation
    @rtype: list of integer
    '''
    codes = gesture.getActionCodes()
    codes.sort()
    return codes
  
  def getMaxActions(self):
    '''
    Gets the maximum number of actions that can be in a L{Gesture} on this 
    input device.
    
    @return: Maximum number of actions per L{Gesture} supported by this device
    @rtype: integer
    '''
    return MAX_KEY_CHORD
    
  def asString(self, gesture): 
    ''' 
    Gets a human readable representation of the given L{Gesture}.
    
    @param gesture: L{Gesture} object to render as text
    @type gesture: L{Gesture}
    @return: Text representation of the L{Gesture} 
    @rtype: string
    '''
    return ','.join([_keyCodeToKeyName(i) for i in gesture.getActionCodes()])
 
  # -------------    begin output related methods  ----------------------------
  def _applyStyle(self, style):
    pass
  
  def createDistinctStyles(self, num_groups, num_layers):
    '''
    Creates distinct styles following the guidelines set forth in the base
    class. 
    
    @param num_groups: Number of sematic groups the requestor would like to
      represent using distinct styles
    @type num_groups: integer
    @param num_layers: Number of content origins (e.g. output originating from
      a background task versus the focus) the requestor would like to represent
      using distinct styles
    @type num_layers: integer   
    @return: Styles
    @rtype: list of L{AEOutput.Style}
    ''' 
    styles = []
    for i in xrange(num_groups*num_layers):
      s = BrlAPIStyle(self.default_style)
      styles.append(s)
    return styles
  
  def close(self):
    '''
    Closes the braille device.
    '''
    self.inputmngr.stop()
    del self.brlconn
    # this call blocks if brltty has been shut down
    #self.brlconn.leaveTtyMode()
    
    self.commands = []
    self.brlconn = None
    
  def sendString(self, text, style):
    '''
    Adds the given string to the text to be output to braille device.

    @param text: String to buffer on the device
    @type text: string
    @param style: Style with which this string should be output; None means no
      style change should be applied
    @type style: integer
    '''
    self.commands.append((Constants.CMD_STRING, text, style))
       
  def sendTalk(self, style=None):
    '''
    Begins output of the buffered text.
    
    @param style: Ignored
    @type style: L{AEOutput.Style}
    '''
    stream = []
    truncateleft = False
    truncateright = False
    
    for command, value, style in self.commands:
      if style is not None and (style.isDirty() or self.last_style != style):
        self._applyStyle(style)
        self.last_style = style
      if command is Constants.CMD_STRING:
        if value is not None:
          stream.append(value)
      elif command is Constants.CMD_TRUNCATE:
        truncateleft = value[0]
        truncateright = value[1]
       
    # get preliminary output string
    outtext = ''.join(stream)
  
    # TODO: change ellipsis symbol to style and do performance increase
    if truncateleft and self.getDefaultStyle().EllipsisLeft:
        outtext = '...' + outtext[3:]
    if truncateright and self.getDefaultStyle().EllipsisRight:
        outtext = outtext[:len(outtext)-3] + '...'
         
    try:
      if style.CaretStyle == style.CaretNone:
        self.brlconn.writeText(outtext)
      # softcode ellipis length in cells
      elif truncateleft and self.caretpos < 4:
        self.brlconn.writeText(outtext)
      elif truncateright and self.caretpos>self.getDefaultStyle().TotalCells-3:
        self.brlconn.writeText(outtext)
      else: # TODO: implement different caret symbols
        self.brlconn.writeText(outtext, cursor=self.caretpos)
    except brlapi.OperationError:
      self.log.warn(_('braille output failed'))
      pass
    
    self.commands = []
    
  def sendTruncate(self, left, right, style):
    '''
    Sends indicators of whether text was truncated on either side of the 
    current line or not. The style object is used by the device in deciding how
    the truncation should be presented.

    @param left: Was text truncated to the left?
    @type left: boolean
    @param right: Was text truncated to the right?
    @type right: boolean
    @param style: Style with which the truncation should be indicated
    @type style: L{AEOutput.Style}
    @raise NotImplementedError: When not overridden in a subclass
    '''
    self.commands.append((Constants.CMD_TRUNCATE, (left,right), style))
    
  def sendCaret(self, pos, style):
    '''
    Sends the current caret position relative to the first cell (zero-offset) 
    on the device. The style object is used by the device in deciding how the
    caret should be presented.
    @note: braille displays are one-based.  We will offset here.
    @param pos: Zero-offset cell position of the caret, up to the device to 
      change to one-offset if need be
    @type pos: string
    @param style: Style with which the caret should be indicated
    @type style: L{AEOutput.Style}
    @raise NotImplementedError: When not overridden in a subclass
    '''
    self.caretpos = pos + 1
        
  def isActive(self):
    '''
    Indicates whether the device has text waiting to be output.

    @return: True if the braille device has text to be output, False otherwise
    @rtype: boolean
    '''
    if len(self.commands) > 0:
      return True
    else:
      return False
  
  def sendStop(self, style=None):
    '''
    Stops braille output immediately.
    
    @param style: Ignored
    @type style: L{AEOutput.Style}
    '''
    self.commands = []
    try:
      self.brlconn.writeText("", cursor=0)
    except brlapi.OperationError:
      self.log.warn(_('braille output failed'))
      pass
    
  def addIndexListener(self, bogus):
    pass