#!/usr/bin/env python
"""
status.py - Emano StatusWindow Class
Author: Sean B. Palmer, inamidst.com
Source: http://inamidst.com/emano/
"""

import curses
import events, window

class Template(object): 
   def __init__(self, template): 
      self.tokens = []
      self.parse(template)

   def parse(self, template): 
      for tok in template.split(): 
         tok = tok.strip()
         if tok: self.tokens.append(tok)

class StatusWindow(window.Window): 
   def __init__(self, editor): 
      self.editor = editor
      self.mappings = {
         events.LEFT: self.moveLeft, 
         events.RIGHT: self.moveRight, 
         events.HOME: self.startOfLine, 
         events.END: self.endOfLine, 
         events.DELETE: self.delChar, 
         events.BACKSPACE: self.deleteChar, 
         events.CTRL_C: self.cancel, 
         events.RETURN: self.collectInput
      }

      self.templates = {
         'default': Template("""
            Emano $. $modchar > $. $uri $. $help $exp $. 
            Line $. $line , $. Pos $. $pos $. 
            $codepoint $. 
            (File $. $doc / $docs )
         """),
         'query': Template("$message : $. $input $. (Emano)"), 
         'notice': Template("$message $exp $. (Emano)")
      }

      self.persist = 0
      self.input = None
      self.inputStart = 0

      # Some defaults that don't really need setting
      self.template = 'default'
      self.args = {}

      # Draw the default template now
      self.show('default')

   # The following correspond with self.mappings
   # @@ StatusWindow.mappings[events.RIGHT] = moveRight?

   def moveLeft(self): 
      pos = self.position()

      # cf. self.deleteChar...
      y, x = self.editor.screen.getyx()
      if self.inputStart and x == self.inputRange[0]: 
         self.inputStart -= 1
         self.refresh()
      elif x > self.inputRange[0]: 
         self.editor.moveCursor(y, x - 1)

   def moveRight(self): 
      pos = self.position()

      y, x = self.editor.screen.getyx()
      
      maxPos = self.inputRange[0] + len(self.input)
      inputRangeEnd = self.inputRange[1] - 1
      if x < min(maxPos, inputRangeEnd): 
         self.editor.moveCursor(y, x + 1)
      elif pos < len(self.input): 
         self.inputStart += 1
         self.refresh()

   def startOfLine(self):       
      y, x = self.editor.screen.getyx()
      self.editor.moveCursor(y, self.inputRange[0])
      self.inputStart = 0
      self.refresh()

   def endOfLine(self): 
      y, x = self.editor.screen.getyx()
      inputWidth = (self.inputRange[1] - self.inputRange[0])

      if not len(self.input) > inputWidth: 
         self.editor.moveCursor(y, self.inputRange[0] + len(self.input))
      else: self.editor.moveCursor(y, self.inputRange[1] - 1)

      start = len(self.input) - inputWidth + 1
      if start > 0: 
         self.inputStart = start

      self.refresh()

   def delChar(self): 
      pos = self.position()
      if pos < len(self.input): 
         self.input = self.input[:pos] + self.input[pos + 1:]
         if self.stream: 
            self.dispatch(self.input)
         self.refresh()

   def deleteChar(self): 
      pos = self.position()
      self.input = self.input[:pos - 1] + self.input[pos:]

      y, x = self.editor.screen.getyx()
      if self.inputStart: 
         self.inputStart -= 1
      elif x > self.inputRange[0]: 
         self.editor.moveCursor(y, x - 1)

      if self.stream: 
         self.dispatch(self.input)
      self.refresh()

   def cancel(self): 
      self.input = ''
      self.inputRange = None
      self.editor.setFocus(self.editor.mainwin)

      if self.args and self.args.has_key('message'): 
         message = 'Cancelled "%s"...' % self.args['message'].lower()
      else: message = 'Cancelled...'
      self.notice(message)

   def collectInput(self): 
      input = self.input
      self.input = ''
      self.inputRange = None

      if not self.stream: 
         self.dispatch(input)
      else: self.stream = False
      self.dispatch = False

      if self.editor.focus is self: 
         self.editor.setFocus(self.savedFocus)
      self.savedFocus = None

   # End of self.mappings correspondences

   def display(self): 
      template = self.templates[self.template]
      self.args = self.args or {}
      savedScreenPosition = self.editor.screen.getyx()

      # Create all the variables
      args = {'$': '$',  '.': ' '}

      y, x, q, p = self.editor.mainwin.position()

      def line(): 
         return str(q + 1)
      args['line'] = line

      def pos(): 
         return str(p + 1)
      args['pos'] = pos

      def modchar(): 
         if len(self.editor.documents) and self.editor.doc.modified(): 
            return '-'
         return '='
      args['modchar'] = modchar

      def help(): 
         if not self.editor.mainwin.typed: 
            return '(Help? Ctrl+H)'
         return False
      args['help'] = help

      def doc(): 
         if self.editor.documents: 
            return str(self.editor.documents.index(self.editor.doc) + 1)
         return str(0)
      args['doc'] = doc

      def docs(): 
         return str(len(self.editor.documents))
      args['docs'] = docs

      def codepoint(): 
         if not self.editor.documents: 
            return False

         try: char = self.editor.doc.line(q)[p]
         except IndexError: pass
         else: 
            cp = ord(char)
            if isinstance(char, unicode): 
               if cp > 0x7F: return '{U+%04X}' % cp
               elif cp == 0x0D: return '{U+000D}'
            elif cp > 0x7F: return r'{\x%02X}' % cp
            elif cp == 0x0D: return r'{\x0D}'
         return False
      args['codepoint'] = codepoint

      special = frozenset(['uri', 'input', 'message', 'exp'])
      # uri -> middle truncate ([...])
      # input -> front truncate
      # message -> end truncate ([...])
      # exp -> expand

      # Substitute the normal args, and user args minus special
      tokens = template.tokens[:]
      for (i, token) in enumerate(tokens): 
         if not token.startswith('$'): 
            continue
         token = token[1:]

         if args.has_key(token): 
            thing = args[token]
            if isinstance(thing, basestring): 
               tokens[i] = thing
            else: tokens[i] = thing() or ''

         elif self.args.has_key(token): 
            tokens[i] = self.args[token]

         elif token not in special: 
            tokens[i] = '?'

      maxy = self.editor.maxy
      maxx = self.editor.maxx

      def remaining(tokens): 
         length = 0
         for token in tokens: 
            if not token.startswith('$'): 
               length += len(token)
         return maxx - length

      # Special
      if '$uri' in tokens: 
         if self.editor.documents and self.editor.doc.uri: 
            uri = self.editor.doc.uri
         else: uri = 'Unsaved File'
         
         i = remaining(tokens)
         if len(uri) > i: # The awesome sliding filename code
            uri = uri[:7] + '[...]' + uri[- i + 12:]
         tokens[tokens.index('$uri')] = uri

      if '$input' in tokens: 
         input = self.input
         i = remaining(tokens)

         before = len(''.join(tokens[:tokens.index('$input')]))
         after = len(''.join(tokens[tokens.index('$input') + 1:]))
         self.inputRange = (before, self.editor.maxx - after)

         if self.inputStart: 
            input = '<' + input[self.inputStart + 1:]

         if len(input) > i: 
            if i < 0: i = 0
            input = input[:-(len(input) - i + 1)] + '>'
         else: input += ' ' * (i - len(input))

         tokens[tokens.index('$input')] = input
      else: self.inputRange = None

      if '$exp' in tokens: 
         exp = ' ' * remaining(tokens)
         tokens[tokens.index('$exp')] = exp

      for (i, token) in enumerate(tokens): 
         if token.startswith('$'): 
            if token[1:] in special: 
               tokens[i] = '@@' + token[1:]

      # Hack to make curses print to the bottom right hand corner
      # Cf. http://swhack.com/logs/2005-03-06#T01-37-04
      self.editor.screen.addch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE)
      self.editor.screen.insch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE)

      status = ''.join(tokens)[:self.editor.maxx]
      self.editor.screen.addstr(maxy + 1, 0, status, curses.A_REVERSE)
      self.editor.screen.refresh()

      # Since we'll be drawing this when, e.g., in the main editor window
      # we need to make sure we restore the cursor to where it was.
      self.editor.moveCursor(*savedScreenPosition)

   def show(self, name, args=None, persist=None, force=False): 
      if self.persist and (not force) and (self.template != name): 
         persist = self.persist - 1
         self.show(self.template, args=self.args, persist=persist)

      if persist is not None: 
         self.persist = persist
      self.template = name
      self.args = args

      self.display()

   def overlay(self, augment, align=None): 
      if align == 'right': 
         savedScreenPosition = self.editor.screen.getyx()

         y = self.editor.maxy + 1
         x = self.editor.maxx - len(augment)
         maxy = self.editor.maxy
         maxx = self.editor.maxx
         self.editor.screen.addch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE)
         self.editor.screen.insch(maxy + 1, maxx - 1, ' ', curses.A_REVERSE)
         self.editor.screen.addstr(y, x, augment, curses.A_REVERSE)

         self.editor.moveCursor(*savedScreenPosition)
      else: raise ValueError("align must be right, for now")

   def refresh(self, augment=None): 
      self.show(self.template, args=self.args)
      if augment is not None: 
         self.overlay(augment, align='right')

   # Legacy methods

   def dialogue(self, message, method, stream=False): 
      self.input = ''
      self.query(message)

      self.savedFocus = self.editor.focus
      self.editor.setFocus(self)
      self.editor.moveCursor(self.editor.maxy + 1, self.inputRange[0])

      # that will set self.inputRange
      self.overlay(str(self.inputRange), align='right')
      self.dispatch = method
      self.stream = stream

   def setInput(self, input): 
      self.input = input
      self.refresh()

   def default(self, augment=None): 
      self.show('default')
      if augment is not None: 
         self.overlay(augment, align='right')

   def query(self, message=None, persist=None): 
      message = message.rstrip(': ')
      self.show('query', {'message': message}, persist, force=True)

   def notice(self, message=None, persist=None): 
      self.show('notice', {'message': message}, persist, force=True)

   # Input and events

   def position(self): 
      y, x = self.editor.screen.getyx()
      return x - self.inputRange[0] + self.inputStart

   def insertChar(self, c): 
      pos = self.position()
      self.input = self.input[:pos] + chr(c) + self.input[pos:]

      y, x = self.editor.screen.getyx()
      if x < self.inputRange[1] - 1: 
         self.editor.moveCursor(y, x + 1)
      else: self.inputStart += 1

      if self.stream: 
         self.dispatch(self.input)
      self.refresh()

   def event(self, e): 
      if self.mappings.has_key(e): 
         self.mappings[e]()
         return True
      else: return False

if __name__=="__main__": 
   print __doc__