#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Urwid escape sequences common to curses_display and raw_display
#    Copyright (C) 2004-2006  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: http://excess.org/urwid/

"""
Terminal Escape Sequences for input and display
"""

import util
import os
import re

import encodings
utf8decode = lambda s: encodings.codecs.utf_8_decode(s)[0]

SO = "\x0e"
SI = "\x0f"

DEC_TAG = "0"
DEC_SPECIAL_CHARS = utf8decode("◆▒°±┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·")
ALT_DEC_SPECIAL_CHARS = u"`afgjklmnopqrstuvwxyz{|}~"

DEC_SPECIAL_CHARMAP = {}
assert len(DEC_SPECIAL_CHARS) == len(ALT_DEC_SPECIAL_CHARS), `DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS`
for c, alt in zip(DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS):
	DEC_SPECIAL_CHARMAP[ord(c)] = SO + alt + SI

SAFE_ASCII_DEC_SPECIAL_RE = re.compile(u"^[ -~%s]*$" % DEC_SPECIAL_CHARS)
DEC_SPECIAL_RE = re.compile(u"[%s]" % DEC_SPECIAL_CHARS)


###################
## Input sequences
###################

class MoreInputRequired(Exception):
	pass

def escape_modifier( digit ):
	mode = ord(digit) - ord("1")
	return "shift "*(mode&1) + "meta "*((mode&2)/2) + "ctrl "*((mode&4)/4)
	

input_sequences = [
	('[A','up'),('[B','down'),('[C','right'),('[D','left'),
	('[E','5'),('[F','end'),('[G','5'),('[H','home'),

	('[1~','home'),('[2~','insert'),('[3~','delete'),('[4~','end'),
	('[5~','page up'),('[6~','page down'),
	('[7~','home'),('[8~','end'),

	('[[A','f1'),('[[B','f2'),('[[C','f3'),('[[D','f4'),('[[E','f5'),
	
	('[11~','f1'),('[12~','f2'),('[13~','f3'),('[14~','f4'),
	('[15~','f5'),('[17~','f6'),('[18~','f7'),('[19~','f8'),
	('[20~','f9'),('[21~','f10'),('[23~','f11'),('[24~','f12'),
	('[25~','f13'),('[26~','f14'),('[28~','f15'),('[29~','f16'),
	('[31~','f17'),('[32~','f18'),('[33~','f19'),('[34~','f20'),

	('OA','up'),('OB','down'),('OC','right'),('OD','left'),
	('OH','home'),('OF','end'),
	('OP','f1'),('OQ','f2'),('OR','f3'),('OS','f4'),
	('Oo','/'),('Oj','*'),('Om','-'),('Ok','+'),

	('[Z','shift tab'),
] + [ 
	# modified cursor keys + home, end, 5 -- [#X and [1;#X forms
	(prefix+digit+letter, escape_modifier(digit) + key)
	for prefix in "[","[1;"
	for digit in "12345678"
	for letter,key in zip("ABCDEFGH",
		('up','down','right','left','5','end','5','home'))
] + [ 
	# modified F1-F4 keys -- O#X form
	("O"+digit+letter, escape_modifier(digit) + key)
	for digit in "12345678"
	for letter,key in zip("PQRS",('f1','f2','f3','f4'))
] + [ 
	# modified F1-F13 keys -- [XX;#~ form
	("["+str(num)+";"+digit+"~", escape_modifier(digit) + key)
	for digit in "12345678"
	for num,key in zip(
		(11,12,13,14,15,17,18,19,20,21,23,24,25,26,28,29,31,32,33,34),
		('f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11',
		'f12','f13','f14','f15','f16','f17','f18','f19','f20'))
] + [
	# mouse reporting (special handling done in KeyqueueTrie)
	('[M', 'mouse')
]

class KeyqueueTrie(object):
	def __init__( self, sequences ):
		self.data = {}
		for s, result in sequences:
			assert type(result) != type({})
			self.add(self.data, s, result)
	
	def add(self, root, s, result):
		assert type(root) == type({}), "trie conflict detected"
		assert len(s) > 0, "trie conflict detected"
		
		if root.has_key(ord(s[0])):
			return self.add(root[ord(s[0])], s[1:], result)
		if len(s)>1:
			d = {}
			root[ord(s[0])] = d
			return self.add(d, s[1:], result)
		root[ord(s)] = result
	
	def get(self, keys, more_available):
		return self.get_recurse(self.data, keys, more_available)
	
	def get_recurse(self, root, keys, more_available):
		if type(root) != type({}):
			if root == "mouse":
				return self.read_mouse_info(keys, 
					more_available)
			return (root, keys)
		if not keys:
			# get more keys
			if more_available:
				raise MoreInputRequired()
			return None
		if not root.has_key(keys[0]):
			return None
		return self.get_recurse(root[keys[0]], keys[1:], more_available)
	
	def read_mouse_info(self, keys, more_available):
		if len(keys) < 3:
			if more_available:
				raise MoreInputRequired()
			return None
		
		b = keys[0] - 32
		x, y = keys[1] - 33, keys[2] - 33  # start from 0
		
		prefix = ""
		if b & 4:	prefix = prefix + "shift "
		if b & 8:	prefix = prefix + "meta "
		if b & 16:	prefix = prefix + "ctrl "

		# 0->1, 1->2, 2->3, 64->4, 65->5
		button = ((b&64)/64*3) + (b & 3) + 1

		if b & 3 == 3:	
			action = "release"
			button = 0
		elif b & MOUSE_RELEASE_FLAG:
			action = "release"
		elif b & MOUSE_DRAG_FLAG:
			action = "drag"
		else:
			action = "press"

		return ( (prefix + "mouse " + action, button, x, y), keys[3:] )


# This is added to button value to signal mouse release by curses_display
# and raw_display when we know which button was released.  NON-STANDARD 
MOUSE_RELEASE_FLAG = 2048  

# xterm adds this to the button value to signal a mouse drag event
MOUSE_DRAG_FLAG = 32


#################################################
# Build the input trie from input_sequences list
input_trie = KeyqueueTrie(input_sequences)
#################################################

_keyconv = {
	-1:None,
	8:'backspace',
	9:'tab',
	10:'enter',
	127:'backspace',
	# curses-only keycodes follow..  (XXX: are these used anymore?)
	258:'down',
	259:'up',
	260:'left',
	261:'right',
	262:'home',
	263:'backspace',
	265:'f1', 266:'f2', 267:'f3', 268:'f4',
	269:'f5', 270:'f6', 271:'f7', 272:'f8',
	273:'f9', 274:'f10', 275:'f11', 276:'f12',
	277:'shift f1', 278:'shift f2', 279:'shift f3', 280:'shift f4',
	281:'shift f5', 282:'shift f6', 283:'shift f7', 284:'shift f8',
	285:'shift f9', 286:'shift f10', 287:'shift f11', 288:'shift f12',
	330:'delete',
	331:'insert',
	338:'page down',
	339:'page up',
	343:'enter',    # on numpad
	350:'5',        # on numpad
	360:'end',
}



def process_keyqueue(codes, more_available):
	"""
	codes -- list of key codes
	more_available -- if True then raise MoreInputRequired when in the 
		middle of a character sequence (escape/utf8/wide) and caller 
		will attempt to send more key codes on the next call.
	
	returns (list of input, list of remaining key codes).
	"""
	code = codes[0]
	if code >= 32 and code <= 126:
		key = chr(code)
		return [key], codes[1:]
	if _keyconv.has_key(code):
		return [_keyconv[code]], codes[1:]
	if code >0 and code <27:
		return ["ctrl %s" % chr(ord('a')+code-1)], codes[1:]
	
	em = util.get_encoding_mode()
	
	if (em == 'wide' and code < 256 and  
		util.within_double_byte(chr(code),0,0)):
		if not codes[1:]:
			if more_available:
				raise MoreInputRequired()
		if codes[1:] and codes[1] < 256:
			db = chr(code)+chr(codes[1])
			if util.within_double_byte(db, 0, 1):
				return [db], codes[2:]
	if em == 'utf8' and code>127 and code<256:
		if code & 0xe0 == 0xc0: # 2-byte form
			need_more = 1
		elif code & 0xf0 == 0xe0: # 3-byte form
			need_more = 2
		elif code & 0xf8 == 0xf0: # 4-byte form
			need_more = 3
		else:
			return ["<%d>"%code], codes[1:]

		for i in range(need_more):
			if len(codes)-1 <= i:
				if more_available:
					raise MoreInputRequired()
				else:
					return ["<%d>"%code], codes[1:]
			k = codes[i+1]
			if k>256 or k&0xc0 != 0x80:
				return ["<%d>"%code], codes[1:]
		
		s = "".join([chr(c)for c in codes[:need_more+1]])
		try:
			return [s.decode("utf-8")], codes[need_more+1:]
		except UnicodeDecodeError:
			return ["<%d>"%code], codes[1:]
		
	if code >127 and code <256:
		key = chr(code)
		return [key], codes[1:]
	if code != 27:
		return ["<%d>"%code], codes[1:]

	result = input_trie.get(codes[1:], more_available)
	
	if result is not None:
		result, remaining_codes = result
		return [result], remaining_codes

	if codes[1:]:
		# Meta keys -- ESC+Key form
		run, remaining_codes = process_keyqueue(codes[1:], 
			more_available)
		if run[0] == "esc" or run[0].find("meta ") >= 0:
			return ['esc']+run, remaining_codes
		return ['meta '+run[0]]+run[1:], remaining_codes
		
	return ['esc'], codes[1:]


####################
## Output sequences
####################

ESC = "\x1b"

CURSOR_HOME = ESC+"[H"

APP_KEYPAD_MODE = ESC+"="
NUM_KEYPAD_MODE = ESC+">"

SWITCH_TO_ALTERNATE_BUFFER = ESC+"[?1049h"
RESTORE_NORMAL_BUFFER = ESC+"[?1049l"

#RESET_SCROLL_REGION = ESC+"[;r"
#RESET = ESC+"c"

REPORT_CURSOR_POSITION = ESC+"[6n"

INSERT_ON = ESC + "[4h"
INSERT_OFF = ESC + "[4l"

def set_cursor_position( x, y ):
	assert type(x) == type(0)
	assert type(y) == type(0)
	return ESC+"[%d;%dH" %(y+1, x+1)

def move_cursor_right(x):
	return ESC+"[%dC" % x

HIDE_CURSOR = ESC+"[?25l"
SHOW_CURSOR = ESC+"[?25h"

MOUSE_TRACKING_ON = ESC+"[?1000h"+ESC+"[?1002h"
MOUSE_TRACKING_OFF = ESC+"[?1002l"+ESC+"[?1000l"

DESIGNATE_G1_SPECIAL = ESC+")0"

_fg_attr = {
	'default':	"0;39",
	'black':	"0;30",
	'dark red':	"0;31",
	'dark green':	"0;32",
	'brown':	"0;33",
	'dark blue':	"0;34",
	'dark magenta':	"0;35",
	'dark cyan':	"0;36",
	'light gray':	"0;37",
	'dark gray':	"1;30",
	'light red':	"1;31",
	'light green':	"1;32",
	'yellow':	"1;33",
	'light blue':	"1;34",
	'light magenta':"1;35",
	'light cyan':	"1;36",
	'white':	"1;37",
}

_fg_attr_xterm = {
	'default':	"39",
	'black':	"30",
	'dark red':	"31",
	'dark green':	"32",
	'brown':	"33",
	'dark blue':	"34",
	'dark magenta':	"35",
	'dark cyan':	"36",
	'light gray':	"37",
	'dark gray':	"90",
	'light red':	"91",
	'light green':	"92",
	'yellow':	"93",
	'light blue':	"94",
	'light magenta':"95",
	'light cyan':	"96",
	'white':	"97",
}

###############################################
# Detect xterm and use non-bold bright colours
if os.environ.get('TERM',None) == 'xterm':
	_fg_attr = _fg_attr_xterm
###############################################


_bg_attr = {
	'default':	"49",
	'black':	"40",
	'dark red':	"41",
	'dark green':	"42",
	'brown':	"43",
	'dark blue':	"44",
	'dark magenta':	"45",
	'dark cyan':	"46",
	'light gray':	"47",
#	'dark gray':	"100",
#	'light red':	"101",
#	'light green':	"102",
#	'yellow':	"103",
#	'light blue':	"104",
#	'light magenta':"105",
#	'light cyan':	"106",
#	'white':	"107",
}

def set_attributes( fg, bg ):
	assert _fg_attr.has_key( fg )
	assert _bg_attr.has_key( bg )
	return ESC+"["+_fg_attr[fg]+";"+_bg_attr[bg]+"m"
	#if fg == 'light gray':
		# xterm workaround
	#return ESC+"[39m"+e
	#return e

