'''
This device provides the braille interface to brlapi (brltty)

@author: Scott Haeger
@organization: IBM Corporation
@copyright: Copyright (c) 2006 IBM Corporation
@license: The BSD License

All rights reserved. This program and the accompanying materials are made
available under the terms of the BSD license which accompanies
this distribution, and is available at
U{http://www.opensource.org/licenses/bsd-license.php}
'''
import brlapi, time, logging, array
import AEOutput, AEInput
from threading import Thread
from i18n import _
from AEConstants import *

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


def _keyCodeToKeyName(code):
  '''
  Gets the name of the given key code by using the brlapi's module dictionary
  to lookup its keycmd value and return its keycmd 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 == value:
        return name
  return ''


class BrlAPIStyle(AEOutput.Style):
  '''
  Overrides the base L{AEOutput.Style} class, filling in 
  fields for supported style properties with their appropriate values.
  
  @ivar CaretStyle: Current caret style  
  @type CaretStyle: L{Setting}    
  @ivar EllipsisRight: User wants right ellipsis shown  
  @type EllipsisRight: L{Setting}    
  @ivar EllipsisLeft: User wants left ellipsis shown  
  @type EllipsisLeft: L{Setting}    
  @ivar CellMask: Missing cell string  
  @type CellMask: L{Setting}        
  '''
  def init(self, dev):
    self.newEnum('CaretStyle', CARET_TWO_BOTTOM, _('Caret Display Style'), 
                {_('No Caret') : CARET_NONE,
                _('Two Bottom Pins') : CARET_TWO_BOTTOM,
                _('Bottom Right Pins') : CARET_BOTTOM_RIGHT,
                _('All Pins') : CARET_ALL},
                _('Defines the caret representation.'))
                
    self.newBool('EllipsisRight', False, _('Ellipsis right?'), 
                _('When set, ellipsis will appear when additional text is to the\
  right of the viewport.'))
    self.newBool('EllipsisLeft', False, _('Ellipsis left?'), 
              _('When set, ellipsis will appear when additional text is to the\
  left of the viewport.'))
    self.newEnum('EllipsisStyle', ELLIPSIS_ELLIPSIS, _('Ellipsis Display Style'), 
                {_('Pin 3 in 3 cells') : ELLIPSIS_ELLIPSIS,
                _('Continuation symbol') : ELLIPSIS_CONTINUATION},
                _('Defines the caret representation.'))
                
    self.newString('CellMask', '', _('Broken Cell String'), 
                  _('String of zeros and ones indicating the functionality of each\
  cell'))
    
  def getGroups(self):
    '''
    Gets configurable absolute settings affecting all output from this device.
    
    @return: Root group of all configurable settings
    @rtype: L{AEState.Setting.Group}
    '''
    root = self.newGroup()
      
    c = root.newGroup(_('Caret'))
    c.append('CaretStyle')
      
    d = root.newGroup(_('Ellipsis'))
    d.append('EllipsisRight')
    d.append('EllipsisLeft')
    d.append('EllipsisStyle')
      
    e = root.newGroup(_('Broken Cells'))
    e.append('CellMask')
   
    return root
    
    
class BrlInputEventListener(Thread):
  '''
  Monitors brlapi for input events from braille device.
  
  @param brldev: a handle to the braille device object
  @type object: L{BrlAPIDevice}
  '''
  def __init__(self, brldev):
    Thread.__init__(self)
    self.brldev = brldev
    self.runflag = True
    
  def run(self):
    '''
    Brlapi input event loop
    '''
    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):
    '''
    Stops the brltty input listener
    '''
    self.runflag = False


class BrlAPIDevice(AEOutput.Braille, AEInput.AEInput):
  '''
  Braille output via Brlapi interface.
  '''
  USE_THREAD = False
  STYLE = BrlAPIStyle
  EllipsisStyles = [ (chr(brlapi.BRL_DOT4 + \
                    brlapi.BRL_DOT5 + brlapi.BRL_DOT6)+ \
                    chr(brlapi.BRL_DOT1 + brlapi.BRL_DOT2 + brlapi.BRL_DOT3+\
                    brlapi.BRL_DOT4 + brlapi.BRL_DOT6)+chr(0),chr(0)+ \
                    chr(brlapi.BRL_DOT4+brlapi.BRL_DOT5 + brlapi.BRL_DOT6)+\
                    chr(brlapi.BRL_DOT1 + brlapi.BRL_DOT2 + brlapi.BRL_DOT3+\
                    brlapi.BRL_DOT4 + brlapi.BRL_DOT6)), \
                    (''.center(3,chr(brlapi.BRL_DOT3))+chr(0),\
                    chr(0)+''.center(3,chr(brlapi.BRL_DOT3)))]
  
  def init(self):
    '''
    Initializes the braille interface.
    
    @ivar brlconn:  connection object to brlapi
    @type brlconn: object    
    @ivar writestruct:  write structure for brlapi.write()
    @type writestruct: object 
    @ivar inputmngr: Input listener thread reference 
    @type inputmngr: object 
    @ivar last_style:  The last style object seen
    @type last_style: integer 
    @ivar commands:  List of output commands
    @type commands: list 
    @ivar caretpos: The one-based cell position of the caret.
    @type caretpos: integer 
    @ivar log:  The logging reference
    @type log: object
    
    @raise AEOutput.InitError: When the device can not be initialized
    '''
    AEInput.AEInput.__init__(self)
    
    self.brlconn = None
    self.writestruct = 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()
      self.writestruct = brlapi.Write()
    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 getbrlconnection(self):
    '''
    Returns the connection object to brltty 
    
    @return: A brlapi connection object
    @rtype: brlapi.Connection
    '''
    return self.brlconn
     
  # -------------    begin input related methods  -----------------------------
  def _loadKeyCmdConstants(self):
    '''
    Reads brlapi constants and puts them in this class 
    '''
    for name, value in brlapi.__dict__.items():
      if name.startswith('KEY_CMD'):
        setattr(self.__class__, name, value)
        
  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))
   # print "keycode=", _keyCodeToKeyName(key-argument)
    
    gesture = AEInput.Gesture(self)
    gesture.addActionCode(command)
    
    # use default arguments or create key_cmd specific ones
    if command == brlapi.KEY_CMD_ROUTE:
      if len(self.default_style.CellMask) == self.default_style.TotalCells:
        # pressed a dead cell
        if self.default_style.CellMask[argument] == '0':
          return
        # adjust cell number by number of missing cells between selected and left end
        for i in range(argument):
          if self.default_style.CellMask[i] == '0':
            argument -= 1
    
    # notify listeners of the gesture with given args at the timestamp
    self._notifyInputListeners(gesture, time.time(), brlarg=(argument,flags))
              
  def _notifyInputListeners(self, gesture, timestamp, **kwargs):
    '''
    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 listener in self.listeners:
      try:
        listener(gesture, timestamp, **kwargs)
      except 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()
    self.inputmngr.join()
    del self.brlconn
    # this call has blocked in past if brltty is offline.
    self.brlconn.leaveTtyMode()
    
    self.commands = []
    self.brlconn = None
    
  def sendString(self, text, style):
    '''
    Adds the given text and associated style to command list.  Text will be 
    output to the braille device in sendTalk.

    @param text: String to be output
    @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((CMD_STRING, text, style))
       
  def sendTalk(self, style=None):
    '''
    Iterates through list of commands and builds output string including user
    selected ellipsis and caret.
    
    @param style: Ignored
    @type style: L{AEOutput.Style}
    '''
    stream = []
    truncateleft = False
    truncateright = False
    # iterate through commands and build pre-output string and set local vars
    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 CMD_STRING:
        if value is not None:
          stream.append(value)
      elif command is CMD_TRUNCATE:
        truncateleft = value[0]
        truncateright = value[1]
       
    # get preliminary output string as an array of unicode
    outarray = array.array('u', ''.join(stream))
    
    if len(self.default_style.CellMask) == self.default_style.TotalCells:
      # insert space for missing cell.  min for protection
      for i in xrange(min(len(outarray),self.default_style.TotalCells)): 
        if self.default_style.CellMask[i] == '0':
          outarray.insert(i, u' ')
          if i < self.caretpos:
            self.caretpos += 1
    elif len(self.default_style.CellMask) != 0:
      self.log.info("Missing cell mask not equal to display length")
    
    # make sure length equals display length before sending to device
    if len(outarray) < self.default_style.TotalCells:
      for i in xrange(self.default_style.TotalCells - len(outarray)):
        outarray.append(u' ')
    elif len(outarray) > self.default_style.TotalCells:
      outarray = outarray[0:self.default_style.TotalCells]
      
    # initialize and/or masks
    andmask = array.array('c', ''.center(self.default_style.TotalCells, \
                    chr(brlapi.BRL_DOT1 + brlapi.BRL_DOT2 +\
                        brlapi.BRL_DOT3 + brlapi.BRL_DOT4 +\
                        brlapi.BRL_DOT5 + brlapi.BRL_DOT6 +\
                        brlapi.BRL_DOT7 + brlapi.BRL_DOT8)))
    ormask = array.array('c', ''.center(40, chr(0)))
    
    # add ellipsis properties to masks
    if truncateleft and self.default_style.EllipsisLeft:
      for i in range(len(self.EllipsisStyles[self.default_style.EllipsisStyle][0])):
        andmask[i] = chr(0)
        ormask[i] = self.EllipsisStyles[self.default_style.EllipsisStyle][0][i]
        outarray[i] = u' '  # corrects brltty/xwindows bug
    if truncateright and self.default_style.EllipsisRight: 
      count = 0
      for i in range(self.default_style.TotalCells-len(self.EllipsisStyles[self.default_style.EllipsisStyle][1]), self.default_style.TotalCells):
        andmask[i] = chr(0)
        ormask[i] = self.EllipsisStyles[self.default_style.EllipsisStyle][1][count]
        outarray[i] = u' '  # corrects brltty/xwindows bug
        count += 1
        
    # add cursor properties to mask and set cursor position
    if self.caretpos <= 0 or self.caretpos > self.default_style.TotalCells:
      self.writestruct.cursor = 0    
    elif self.default_style.CaretStyle is CARET_NONE:
      self.writestruct.cursor = 0  
    elif self.default_style.CaretStyle is CARET_TWO_BOTTOM:
      ormask[self.caretpos-1] = chr(brlapi.BRL_DOT7 + brlapi.BRL_DOT8)
      self.writestruct.cursor = self.caretpos
    # TODO: check pins for CaretBottomRight
    elif self.default_style.CaretStyle is CARET_BOTTOM_RIGHT:
      ormask[self.caretpos-1] = chr(brlapi.BRL_DOT6 + brlapi.BRL_DOT8)
      self.writestruct.cursor = self.caretpos
    else: # style.CaretStyle is style.CaretAll:
      ormask[self.caretpos-1] = chr(brlapi.BRL_DOT1 + brlapi.BRL_DOT2 +\
                        brlapi.BRL_DOT3 + brlapi.BRL_DOT4 +\
                        brlapi.BRL_DOT5 + brlapi.BRL_DOT6 +\
                        brlapi.BRL_DOT7 + brlapi.BRL_DOT8)
      self.writestruct.cursor = self.caretpos
                        
    # set write structure fields
    self.writestruct.attrAnd = andmask.tostring()
    self.writestruct.attrOr = ormask.tostring()
    self.writestruct.text = outarray.tounicode() 
    self.writestruct.regionBegin = 1
    self.writestruct.regionSize = self.default_style.TotalCells
    
    # write to device  
    try:
       self.brlconn.write(self.writestruct) 
    except brlapi.OperationError:
       self.log.warn(_('braille output failed'))
    
    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((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
    