'''
This perk provides the basic braille user interface for LSR

@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 useful modules for Perks
import Perk, Task.Tools
from POR import POR
from i18n import bind, _
from AEConstants import *

# metadata describing this Perk
__uie__ = dict(kind='perk', tier=None, all_tiers=True)

# to support translation of strings in this Perk, uncomment the following line
# and provide the proper domain and path to your translation file
# _ = bind(domain, locale_dir)

class BasicBraillePerkState(Perk.PerkState):
  '''
  Settings for BasicBraillePerk
  @ivar Overlap: Number of original chars remaining on page forward/backward
  @type Overlap: L{Setting}
  @ivar LeftPadding: remaining cells on left side during scrolling
  @type LeftPadding: L{Setting}
  @ivar RightPadding: remaining cells on right side during scrolling
  @type RightPadding: L{Setting}
  @ivar SnapOnUpdate: snap braille display to previous location upon update
  @type SnapOnUpdate: L{Setting}
  @ivar SnapToWord: snap braille display to nearest word upon paging
  @type SnapToWord: L{Setting}
  '''
  def init(self):
    '''
    Initializes L{Setting} objects.
    '''
    self.newBool('SnapOnUpdate', False, _('Snap on update?'), 
                _('When set, after any type of scrolling, display snaps to original position upon an update.'))
    self.newNumeric('Overlap', 0, _('Overlapping cells'), 0, 10, 0)
    self.newBool('SnapToWord', False, _('Snap to word?'), 
                _('When set, display snaps to nearest word on previous or next chunk of text.'))
    self.newNumeric('LeftPadding', 0, _('Left Padding'), 0, 10, 0)
    self.newNumeric('RightPadding', 0, _('Right Padding'), 0, 10, 0)
    
   
  def getGroups(self):
    '''
    Creates configuration groups from pre-defined L{Setting} objects.
    @return: root setting group
    @rtype: L{AEState.Setting.Group}
    '''
    root = self.newGroup()
    
    g = root.newGroup(_('Scrolling'))
    g.append('SnapOnUpdate')
    
    g = root.newGroup(_('Display Forwarding'))
    g.append('Overlap')
    g.append('SnapToWord')
    
    g = root.newGroup(_('Padding'))
    g.append('LeftPadding')
    g.append('RightPadding')
    
    return root
    
  
class BasicBraillePerk(Perk.Perk):
  '''
  Defines a default braille user interface.
  
  @ivar left_slice: Left slice index for current_text including scroll offset
  @type left_slice: integer
  @ivar pre_left_slice: Left slice index into current_text before scroll_offset
  @type pre_left_slice: integer
  @ivar scroll_offset: Scroll offset relative to pre_left_slice
  @type scroll_offset: integer
  @ivar abscaretpos: Index of the caret position within current_text
  @type abscaretpos: integer
  @ivar current_text: String containing entire output from accessible
  @type current_text: string
  
  '''
  # defines the class to use for state information in this Perk
  STATE = BasicBraillePerkState
  
  def init(self):
    '''
    Registers L{Task}s to handle focus, caret, selector, state, and 
    property change events. 
    '''
    self.left_slice = 0
    self.pre_left_slice = 0
    self.scroll_offset = 0
    self.abscaretpos = -1
    self.current_text = ""
    
    # set the output device                       
    self.setPerkIdealOutput('braille')
    
    # register braille events handlers
    self.registerTask(HandleSelectorChange('default selector'))
    self.registerTask(HandleCaretChange('default caret'))
    self.registerTask(HandleFocusChange('default focus'))
    
    # register named tasks
    self.registerTask(OutputText('braille output'))
    
    # register braille named tasks for input events
    self.registerTask(HandleScrollForward('scroll forward'))
    self.registerTask(HandleScrollBackward('scroll backward'))
    self.registerTask(HandlePageForward('page forward'))
    self.registerTask(HandlePageBackward('page backward'))
    self.registerTask(HandleTouchCursor('touch cursor'))
                         
    #register braille device commands
    #TODO: send audible error
    brlindev = self.getInputDevice('BrlAPIDevice')
    
    self.registerCommand(brlindev, 'scroll backward', True, 
                         [brlindev.KEY_CMD_FWINLT])
    self.registerCommand(brlindev, 'scroll forward', True, 
                         [brlindev.KEY_CMD_FWINRT])
    self.registerCommand(brlindev, 'page backward', True, 
                         [brlindev.KEY_CMD_FWINRTSKIP])
    self.registerCommand(brlindev, 'page forward', True, 
                         [brlindev.KEY_CMD_FWINLTSKIP])            
    self.registerCommand(brlindev, 'touch cursor', True, 
                         [brlindev.KEY_CMD_ROUTE])
                                                     
  def getDescription(self):
    '''
    Get Description of this Perk
    
    @return: description
    @rtype: String
    '''
    return _('Basic Braille Perk manages basic braille output')
  
  def setAbsCaretPos(self, pos):
    '''
    Set absolute caret position
    '''
    if pos >= -1 or pos < len(self.current_text):
      self.abscaretpos = pos
      
  def getRelCaretPos(self):
    '''
    Get relative (to device display) caret position
    
    @return: caret position
    @rtype: Integer
    '''
    # -1 (0 on device) is special case where we don't show cursor
    if self.abscaretpos == -1:
      return -1
    else:
      return max(-1, self.abscaretpos - self.getLeftSlice(40)) 
     
  # TODO:  move to Output text as private method
  def getLeftSlice(self, totalcells):
    '''
    Get the leftmost slice indice for the current braille output
    
    @return: Left slice index
    @rtype: int
    '''  
    state = self.getState()
    if self.task_por.char_offset-self.pre_left_slice < state.LeftPadding:
      retval = max(0,self.task_por.char_offset-state.LeftPadding)
    elif self.task_por.char_offset-self.pre_left_slice >= totalcells-state.RightPadding:
      retval = self.task_por.char_offset-totalcells+state.RightPadding+1
    else:
      retval = self.pre_left_slice
    
    # set before applying scroll_offset
    self.pre_left_slice = retval
    
    # apply scroll offset
    if self.scroll_offset > 0:
      retval += self.scroll_offset
    elif self.scroll_offset < 0:
      retval = max(0, retval + self.scroll_offset)
      
    return retval
    
class HandleTouchCursor(Task.InputTask):
  '''
  Handle touch cursor sets caret at given location in text accessibles or
  left clicks the center of the given accessible
  '''
  def execute(self, **kwargs):
    cellnumber = kwargs['brlarg'][0]
  
    newoffset = self.perk.pre_left_slice + self.perk.scroll_offset + cellnumber
    self.perk.pre_left_slice += self.perk.scroll_offset  
    if not self.perk.getState().SnapOnUpdate:
      self.perk.scroll_offset = 0
         
    if newoffset < 0:
      newoffset = 0
    elif newoffset > len(self.perk.current_text)-1:
      newoffset = len(self.perk.current_text) - 1
      
    # adjust current por and send touch cursor 
    self.task_por.char_offset = newoffset
    self.setTouchCursor(EVENT_SYNTHMOUSE_B1C, self.task_por)
    
  
class HandleScrollForward(Task.InputTask):
  '''
  Scrolls Braille viewport to right
  '''
  def execute(self, **kwargs):
    style=self.getStyle()
    if style is None:
      return
    state=self.perk.getState()
   
    if self.perk.pre_left_slice + self.perk.scroll_offset < \
          len(self.perk.current_text)-style.TotalCells+state.RightPadding:
      self.perk.scroll_offset += 1
    self.doTask("braille output")
     
class HandleScrollBackward(Task.InputTask):
  '''
  Scrolls Braille viewport to left
  '''
  def execute(self, **kwargs):
    if self.perk.pre_left_slice + self.perk.scroll_offset > 0:
      self.perk.scroll_offset -= 1
    self.doTask("braille output")
    
class HandlePageForward(Task.InputTask):
  '''
  Scrolls Braille viewport to right one display length minus overlap
  '''
  def execute(self, **kwargs):
    style=self.getStyle()
    if style is None:
      return
    state=self.perk.getState()
    
    ctlen = len(self.perk.current_text)
    
    if  2*style.TotalCells+self.perk.pre_left_slice+ \
           self.perk.scroll_offset-state.Overlap < ctlen + state.RightPadding:
      self.perk.scroll_offset += style.TotalCells-state.Overlap
    else:
      self.perk.scroll_offset = ctlen + state.RightPadding - style.TotalCells - self.perk.pre_left_slice
      
    if state.SnapToWord:
      found = False
      wordadj = 0
      for i in xrange(self.perk.scroll_offset+self.perk.pre_left_slice, \
                      -1, -1):
        if self.perk.current_text[i] == ' ' or i == 0:
          if i != 0:
            wordadj += 1
          found = True
          break
        else:
          wordadj -= 1 
      if found:
        self.perk.scroll_offset += wordadj
    
    self.doTask("braille output")
  
class HandlePageBackward(Task.InputTask):
  '''
  Scrolls Braille viewport to left one display length minus overlap
  '''
  def execute(self, **kwargs):
    style=self.getStyle()
    if style is None:
      return
    state=self.perk.getState()
    
    if  self.perk.pre_left_slice+self.perk.scroll_offset-(style.TotalCells-state.Overlap) > 0:
      self.perk.scroll_offset -= style.TotalCells-state.Overlap
    else:
      self.perk.scroll_offset = (self.perk.pre_left_slice * -1)
      
    if state.SnapToWord:
      found = False
      wordadj = 0
      for i in xrange(self.perk.scroll_offset+self.perk.pre_left_slice, \
                      -1, -1):
        if self.perk.current_text[i] == ' ' or i == 0:
          if i != 0:
            wordadj += 1
          found = True
          break
        else:
          wordadj -= 1 
      if found:
        self.perk.scroll_offset += wordadj
      
    self.doTask("braille output")
    
class HandleFocusChange(Task.FocusTask):
  '''  
  Task that handles a L{AEEvent.FocusChange}.
  '''  
  def executeGained(self, por, **kwargs):
    '''
    @param por: Point of regard for the new focus
    @type por: L{POR}
    '''
    self.perk.scroll_offset = 0
    self.perk.current_text = ""
    self.doTask("braille output")
    return True

class HandleSelectorChange(Task.SelectorTask):
  ''' 
  Task that handles a L{AEEvent.SelectorChange}.
  '''
  def executeActive(self, por, text, **kwargs):
    '''
    Announces the text of the active item.
    
    @param por: Point of regard where the selection event occurred
    @type por: L{POR}
    @param text: The text of the active item
    @type text: string
    '''  
    # update current text
    self.perk.current_text = self.getItemText()
    self.perk.scroll_offset = 0
    # Don't show cursor
    self.perk.setAbsCaretPos(-1)
    # send out line of text
    self.doTask("braille output")
      
  def executeText(self, por, text, **kwargs):
    '''
    Announces the localized word selected or unselected followed by the total 
    number of characters in the selection.
    
    @param por: Point of regard where the selection event occurred
    @type por: L{POR}
    @param text: The currently selected text
    @type text: string
    '''
    pass  
    
class HandleCaretChange(Task.CaretTask):
  '''  
  Task that handles a L{AEEvent.CaretChange}. The L{execute} method performs
  actions that should be done regardless of whether text was inserted or
  deleted or the caret moved. The other three methods handle these more
  specific cases.
  
  @ivar changed: Tracks whether the last caret move event was a caused by a
    change (insert/delete) or just navigation
  @type changed: boolean
  '''
  def init(self):
    pass
    
  def executeInserted(self, por, text, text_offset, **kwargs):
    '''
    Outputs the entire text to braille display in a managed fashion (overlaps,
    caret position, padding etc.).  Snaps to original position (before any 
    scrolling) if requested.
    
    @param por: Point of regard where the caret event occurred
    @type por: L{POR}
    @param text: The text inserted
    @type text: string
    @param text_offset: The offset of the inserted text
    @type text_offset: integer
    '''
    # update current text
    style = self.getStyle()
    if style is None:
      return
    self.perk.current_text = self.getItemText()
    if self.perk.getState().SnapOnUpdate:
      self.perk.scroll_offset = 0
    self.perk.setAbsCaretPos(por.char_offset)
    # send out line of text
    self.doTask("braille output") 
    
  def executeMoved(self, por, text, text_offset, **kwargs):
    '''
    Outputs the entire text to braille display in a managed fashion (overlaps,
    caret position, padding etc.).  Snaps to original position (before any 
    scrolling) if requested.
    
    @param por: Point of regard where the caret event occurred
    @type por: L{POR}
    @param text: The text passed during the move
    @type text: string
    @param text_offset: The offset of the new caret position
    @type text_offset: integer
    '''
    # update current text
    self.perk.pre_left_slice = 0
    style = self.getStyle()
    if style is None:
      return
    self.perk.current_text = self.getItemText()
    if self.perk.getState().SnapOnUpdate:
      self.perk.scroll_offset = 0
    self.perk.setAbsCaretPos(por.char_offset)
    # send out line of text
    self.doTask("braille output")
    
  def executeDeleted(self, por, text, text_offset, **kwargs):
    '''
    Outputs the entire text to braille display in a managed fashion (overlaps,
    caret position, padding etc.).  Snaps to original position (before any 
    scrolling) if requested.
    
    @param por: Point of regard where the caret event occurred
    @type por: L{POR}
    @param text: The text delete
    @type text: string
    @param text_offset: The offset of the delete text
    @type text_offset: integer
    '''
    # update current text
    style = self.getStyle()
    if style is None:
      return
    self.perk.current_text = self.getItemText()
    if self.perk.getState().SnapOnUpdate:
      self.perk.scroll_offset = 0
    self.perk.setAbsCaretPos(por.char_offset)
    # send out line of text
    self.doTask("braille output")

  def update(self, por, text, text_offset, added, **kwargs):
    '''
    Updates state variables and relies on other methods to handle any output.
    
    @param por: Point of regard where the caret event occurred
    @type por: L{POR}
    @param text: The text inserted, deleted or the line of the caret
    @type text: string
    @param text_offset: The offset of the inserted/deleted text or the line 
      offset when movement only
    @type text_offset: integer
    @param added: True when text added, False when text deleted, and None 
      when event is for caret movement only
    @type added: boolean
    '''
    # update all state variables 
    style = self.getStyle()
    if style is None:
      return
    self.perk.current_text = self.getItemText()
    if self.perk.getState().SnapOnUpdate:
      self.perk.scroll_offset = 0
    self.perk.setAbsCaretPos(por.char_offset)
   
 
class OutputText(Task.Task):
  '''
  Output text to braille device in a managed fashion.  Relays caret position
  and truncation information to device.
  '''
  def execute(self, **kwargs):
    style = self.getStyle()
    if style is None:
      return
    state = self.perk.getState()
    
    # set caret position. 
    caretpos = self.perk.getRelCaretPos()
  
    if caretpos < 0 or caretpos >= style.TotalCells:
      self.send(CMD_CARET, -1, talk=False)
    else:
      self.send(CMD_CARET, caretpos, talk=False)
  
    leftslice = self.perk.getLeftSlice(style.TotalCells)
    #Note: rightslice >= len(current_text) does not throw exception
    rightslice = leftslice+style.TotalCells
    
    # truncation indicators for ellipsis
    self.send(CMD_TRUNCATE, \
        (leftslice>0, rightslice<len(self.perk.current_text)), talk=False) 
        
    # send text to device
    self.sayItem(text=self.perk.current_text[leftslice:rightslice])
    