#!/usr/bin/python

"""
============================================
vnc_client.py 
vnc_client is part of VNC60 for symbian S60 phones
(C) 2007 Michele Andreoli

How it works:
-------------
vnc_client.py connects itself to the bluetooth service called 'VNC',
created by the vnc_server.py (running on the linux side)
and send commands to it: mouse movements, button click,
screenshot requests, shell command requests, amarok control
commands, etc

vnc_client.py use a socket self-made library (see my stream.py)
able to incapsulate a simple file-transfer protocol

thread locks are used in order to use the stream channel
with consistency

============================================
"""

from version import VERSION
from version import APPNAME


import sys
# refuse to work out of the phone :-)

if sys.platform.find("symbian") <0:
	print "Please, run this program on the phone"
	sys.exit(1)

import appuifw
import e32,sys
from graphics import Image
from graphics import screenshot as phone_screenshot
from key_codes import *
import thread
import traceback
from audio import say
import time
import os
from random import uniform
from stream import Stream

#-----------------
# routines
#-----------------

def note(msg):
	global cv
	sys.stderr.write(msg+'\\n')
	appuifw.note(unicode(msg))
	e32.ao_yield()
# writes messages on the red bar

def status(msg):
        global IMG,cv,landscape
	(w,h)=cv.size
	print msg
	if landscape: return

	size=12
	rect=((0,h-size),(w,h))
	status_row=142
		
	IMG.rectangle(rect, fill=0xff0000)
	IMG.text( (0,status_row),u'%s'%msg)
	update(rect)
        e32.ao_yield()


# this class is able to buffer keycodes

class Keyboard(object):
    def __init__(self,onevent=lambda:None):
        self._keyboard_state={}
        self._downs={}
        self._onevent=onevent
	self.limit=4
    def handle_event(self,event):
        if event['type'] == appuifw.EEventKeyDown:
            xcode=event['scancode']
	    print "key=%2d" % xcode
            if not self.is_down(xcode):
		# I will limit the buffer 
		if self._downs.get(xcode,0)>self.limit:
                	self._downs[xcode]=0
                self._downs[xcode]=self._downs.get(xcode,0)+1
            self._keyboard_state[xcode]=1
        elif event['type'] == appuifw.EEventKeyUp:
            self._keyboard_state[event['scancode']]=0
        self._onevent()
    def is_down(self,scancode):
        return self._keyboard_state.get(scancode,0)
    def pressed(self,scancode):
        if self._downs.get(scancode,0):
            self._downs[scancode]-=1
            return True
        return False
    def flush(self):
	self.state={}
	self.buffer={}

# the app exit soft key
	
def exit_key_handler():
	global running
	running=0


# restore the UI values

def restore_ui():
	global oldBody,oldMenu,OldTitle,oldExit,oldScreen
	appuifw.app.body=oldBody
	appuifw.app.menu=oldMenu
	appuifw.app.title=oldTitle
	appuifw.app.exit_key_handler=oldExit	
	appuifw.app.screen=oldScreen	

# screenshot wrapper

def screenshot_request(mode):
	if mode=='x11':
		get_screenshot()
	else:
		send_screenshot()

# Phone->X11 screenshot

def send_screenshot():
        global screenshot,interval
        global s,s_lock,running
        if screenshot['phone']==0:
                e32.ao_sleep(interval['phone'],send_screenshot)
                return
        if s_lock.locked():
                print "send_screenshot(): *** locked *** - rescheduled"
                e32.ao_sleep(uniform(0.01,0.09),send_screenshot)
                return
	img=phone_screenshot()
	img.save(screen['phone'])
	del img
        s_lock.acquire()
        s.write("SEND_SCREEN")
        s.sendfile(screen['phone'])
        s_lock.release()
        e32.ao_yield()
        date=time.ctime()
        status("shot sent %s" % date)
        e32.ao_sleep(interval['phone'],send_screenshot)

# take X11 screenshots at regular intervals
# and re-schedule itself

def get_screenshot():
	global cv, screenshot,interval
	global s,s_lock,running
	global IMG
	if screenshot['x11']==0: 
		e32.ao_sleep(interval['x11'],get_screenshot)
		return
	if s_lock.locked():
		print "get_screenshot(): *** locked *** - rescheduled"
		e32.ao_sleep(uniform(0.01,0.09),get_screenshot)
		return
	# send info about required screenshot: x,y,rotation
	w,h=cv.size;
	s_lock.acquire()
	if appuifw.app.screen=="full":
		(x,y)=(h*3/4,h)
		s.write("GET_SCREEN,%s,%s,%s" %(x,y,90) ) 
	elif appuifw.app.screen=="normal":
		(x,y)=(w,w*3/4)
		s.write("GET_SCREEN,%s,%s,%s" %(x,y,0) ) 
	s.recfile(screen['x11'])
	s_lock.release()
	e32.ao_yield()
        img=Image.open(screen['x11'])
        IMG.blit(img,(0,0))
        date=time.ctime()
        print "%s -> %s" % (img.size,cv.size) 
	update( ((0,0),img.size))
        status("%s" % date)
        del img
	e32.ao_sleep(interval['x11'],get_screenshot)
	update()

#----------------
# app menu's callbacks
#----------------

def menu_toggle_screenshot_cb(mode='x11'):
	global screenshot

	if screenshot[mode]:
		screenshot[mode]=0
	else:
		screenshot[mode]=1
		e32.ao_sleep(0.2,screenshot_request(mode))
	status("%s screenshot enabled %d " % (mode,screenshot[mode]))

def menu_toggle_landascape_cb():
	global IMG,cv,background_color,screenshot
	global landscape
	save=screenshot['x11']
	screenshot['x11']=0
	try:
		del IMG
	except:
		pass

	if appuifw.app.screen=='full':
		appuifw.app.screen='normal'
		landscape=0
	elif appuifw.app.screen=='normal':
		landscape=1
		appuifw.app.screen='full'
	IMG=Image.new(cv.size)
	IMG.clear(background_color)
	status("screen size  %s " % str(cv.size))
	update()
	screenshot['x11']=save
	if screenshot['x11']:
		e32.ao_sleep(0.05,get_screenshot)

def menu_refresh_cb(mode=None):
	global  interval
	r=interval['x11']
	if mode==None:
		r=appuifw.query(u'seconds','float',r)
	elif mode=='faster':
		r*=0.666
	elif mode=='slower':
		r*=1.5
	interval['x11']=r
	status("refresh time %f" % r)

# shell command requests to the server
# NB: an output is required, or client will blocks on read

def menu_command_cb():
	global s_lock,s
        cmd=appuifw.query(u'remote cmd','text',u'say ciao remo')
	s_lock.acquire()
	s.write('CMD,%s'%cmd); r=s.read()
	s_lock.release()
        status("-> %s" % r)

# gather some info about the linux peer

def get_linux_info():
	# collect some info about remote system
	s_lock.acquire()
	s.write('CMD,hostname'); host=s.read()
	s.write('CMD,uname -r'); release=s.read()
	s_lock.release()
	
	return "hostname %s,%s" %(host,release) 

# update the phone canvas: IMG->Canvas

def update(rect=None):
	global cv,IMG
	if rect==None:
		rect=( (0,0), cv.size )
	#print "update: %s" % str(rect)
	cv.blit(IMG,target=rect[0],source=rect)
	e32.ao_yield()

#---------------
# HELPER 
#---------------

# X11 mouse motion requests

def mouse(cmd):
	status("mouse %s"%cmd)
	send(cmd)

# amarok requests 

def sound(cmd):
	global s,s_lock
	s_lock.acquire()
	s.write(cmd); r=s.read()
	s_lock.release()
	status("[%s] %s"%(cmd,r))
	#e32.ao_yield()

# send a generic string to the linux side 

def send(cmd):
	global s,s_lock
	if s_lock.locked():
		print " send(%s): locked!!!!" % cmd
	s_lock.acquire()
	s.write(cmd)
	s_lock.release()
	#e32.ao_yield()

#---------------
# MAIN
#---------------

e32.reset_inactivity()

# connect to the 'VNC' bluetooth service

s=Stream(mode='BT')
s.connect(service='VNC')
#s.connect(host="00:11:f6:03:3e:89", port=1)
e32.ao_yield()

# this is the lock guarding the stream 's'

s_lock=thread.allocate_lock()

#------------------------------
# configuration parameters
#------------------------------

screen={ 'x11':"e:\\Images\\x11.jpg", 'phone':"e:\\Images\\shot.jpg" }
interval={'x11':1.5, 'phone':1}
screenshot={'x11':1, 'phone':0}

background_color=0x000099
running=1
landscape=0

# backup UI

oldBody=appuifw.app.body
oldExit=appuifw.app.exit_key_handler
oldTitle=appuifw.app.title
oldMenu=appuifw.app.menu
oldScreen=appuifw.app.screen

# the new UI

keyboard=Keyboard()
appuifw.app.screen='normal'

appuifw.app.body=cv=appuifw.Canvas(
   event_callback=keyboard.handle_event,
)

# the canvas's IMG zbuffer

IMG=Image.new(cv.size)
IMG.clear(background_color)

# the app's MENU

help_msg=[
u"%6s > %s"%("arrows","mouse"),
u"%6s > %s"%("select","click"),
u"%6s > %s"%("0","shot on/off"),
u"%6s > %s"%("backsp","exit"),
u"%6s > %s"%("#","landscp on/off"),
u"%6s > %s"%("5","play/pause"),
u"%6s > %s"%("4,5","prev/next"),
u"%6s > %s"%("2,8","vol+/vol-"),
]

appuifw.app.title=u"%s v%s"%(APPNAME,VERSION)
appuifw.app.exit_key_handler=exit_key_handler	

appuifw.app.menu=[
(u'Screen: X11->Phone', lambda: menu_toggle_screenshot_cb('x11')),
#(u'Screen: Phone->X11', lambda: menu_toggle_screenshot_cb('phone')),
(u'Refresh rate',  menu_refresh_cb ),
(u'Linux Cmd', menu_command_cb ),
(u'Landscape/Portrait', menu_toggle_landascape_cb),
(u'Help', lambda: appuifw.popup_menu(help_msg,u'help') ),
(u'About', lambda: appuifw.note(u'mulinux.sunsite.dk/python') ),
(u'Exit', exit_key_handler),
]

# startup info 

remote_info=get_linux_info()

# init finished
status(remote_info)
update()

#-----------
# MAIN LOOP 
#-------------

# schedule some daemon

e32.ao_sleep( interval['x11'], get_screenshot )
#e32.ao_sleep( interval['phone'], send_screenshot )
e32.ao_sleep(3,e32.reset_inactivity)

try:
    while running:
	#-----------------------
	#  X11 mouse events 
	#-----------------------
        if keyboard.is_down(EScancodeLeftArrow): mouse("LEFT")
        if keyboard.is_down(EScancodeRightArrow): mouse("RIGHT")
        if keyboard.is_down(EScancodeUpArrow): mouse("UP")
        if keyboard.is_down(EScancodeDownArrow): mouse("DOWN")

	# ------------------------- 
	# the ButtonPress 1
	# ------------------------- 

        if keyboard.pressed(EScancodeSelect): mouse("SELECT")

        #-----------------------
        #  sound control 
        #-----------------------

        if keyboard.pressed(EKey5): sound("PLAY")
        if keyboard.pressed(EKey4): sound("PREVIOUS")
        if keyboard.pressed(EKey6): sound("NEXT")
        if keyboard.pressed(EKey2): sound("VOLUME-UP")
        if keyboard.pressed(EKey8): sound("VOLUME-DOWN")

        #-----------------------
        #  others 
        #-----------------------

	# the "C" key: exit shortcut
        if keyboard.is_down(EScancodeBackspace): 
		send('END')
		running=0
		break

	# the '1' key: test purpose
        if keyboard.pressed(EKey1): 
		send("PING")
		status("X11=%d|Phone=%d|frq=%f|lndscp=%s" %(screenshot['x11'],
		screenshot['phone'],
		interval['x11'],landscape)
		)		
	# Toggle Landscape/Portrait shortcut (key #)	
        elif keyboard.pressed(EScancodeHash): 
		menu_toggle_landascape_cb()
	# Toggle X11-ScreenShot on/off shortcut (key 0)	

        elif keyboard.pressed(EScancode0): 
		menu_toggle_screenshot_cb('x11')
	# fall back to nothing	
	else: 
		pass
	e32.ao_yield()
    s.close()
    note("Thank You!")
except:
	error_type, error, traceback = sys.exc_info()
	print "exception in main loop: %s" % error
	
    	s.write('END')
    	s.close()	

restore_ui 
print "%s (rustic VNC) client ended" % APPNAME
# End
