# Copyright (c) 2005 Nokia Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import sys
import os
import socket
import e32
import thread
import glob

class socket_stdio:
    def __init__(self,sock):
        self.socket   = sock
        self.writebuf = []
        self.readbuf  = []
        self.history  = ['pr()']
        self.histidx  = 0
        self.socket_thread_id = thread.get_ident()
    def _read(self,n=1):
        if thread.get_ident() != self.socket_thread_id:
            raise IOError("read() from thread that doesn't own the socket")
        try:
            return self.socket.recv(n)
        except socket.error:
            raise EOFError("Socket error: %s %s"%(sys.exc_info()[0:2]))
    def _write(self,str):
        try:
            return self.socket.send(str.replace('\n','\r\n'))
        except socket.error:
            raise IOError("Socket error: %s %s"%(sys.exc_info()[0:2]))
    def read(self,n=1):
        # if read buffer is empty, read some characters.
        # try to read at least 32 characters into the buffer.
        if len(self.readbuf)==0:
            chars=self._read(max(32,n))
            self.readbuf=chars
        readchars,self.readbuf=self.readbuf[0:n],self.readbuf[n:]
        return readchars
    def _unread(self,str):
        self.readbuf=str+self.readbuf
    def write(self,str):
        self.writebuf.append(str)
        if '\n' in self.writebuf:
            self.flush()
    def flush(self):
        if thread.get_ident() == self.socket_thread_id:
            self._write(''.join(self.writebuf))
            self.writebuf=[]
    def readline(self,n=None):
        buffer=[]
        while 1:
            chars=self.read(32)
            for i in xrange(len(chars)):
                ch=chars[i]
            	if ch == '\n' or ch == '\r':   # return
            	    buffer.append('\n')
            	    self.write('\n')
                    self._unread(chars[i+1:]) #leave
                    line=''.join(buffer)
                    histline=line.rstrip()
                    if len(histline)>0:
                        self.history.append(histline)
                        self.histidx=0
            	    return line
            	elif ch == '\177' or ch == '\010': # backspace
                    if len(buffer)>0:
                        self.write('\010 \010') # erase character from the screen
                        del buffer[-1:] # and from the buffer
                elif ch == '\004': # ctrl-d
                    raise EOFError
                elif ch == '\020' or ch == '\016': # ctrl-p, ctrl-n                  
                    self.histidx=(self.histidx+{
                        '\020':-1,'\016':1}[ch])%len(self.history)
                    #erase current line from the screen
                    self.write(('\010 \010'*len(buffer)))
                    buffer=list(self.history[self.histidx])
                    self.write(''.join(buffer))
                    self.flush()
                elif ch == '\025':
                    self.write(('\010 \010'*len(buffer)))
                    buffer=[]
            	else:
            	    self.write(ch)
            	    buffer.append(ch)
            	if n and len(buffer)>=n:
            	    return ''.join(buffer)
            self.flush()

def _readfunc(prompt=""):
    sys.stdout.write(prompt)
    sys.stdout.flush()
    return sys.stdin.readline().rstrip()

def connect(address=None):
    """Form an RFCOMM socket connection to the given address. If
    address is not given or None, query the user where to connect. The
    user is given an option to save the discovered host address and
    port to a configuration file so that connection can be done
    without discovery in the future.

    Return value: opened Bluetooth socket or None if the user cancels
    the connection.
    """
    
    # Bluetooth connection
    sock=socket.socket(socket.AF_BT,socket.SOCK_STREAM)

    if not address:
        import appuifw
        CONFIG_DIR='c:/system/apps/python'
        CONFIG_FILE=os.path.join(CONFIG_DIR,'btconsole_conf.txt')
        try:
            f=open(CONFIG_FILE,'rt')
            try:
                config=eval(f.read())
            finally:
                f.close()
        except:
            config={}
    
        address=config.get('default_target','')
    
        if address:
            choice=appuifw.popup_menu([u'Default host',
                                       u'Other...'],u'Connect to:')
            if choice==1:
                address=None
            if choice==None:
                return None # popup menu was cancelled.    
        if not address:
            print "Discovering..."
            try:
                addr,services=socket.bt_discover()
            except socket.error, err:
                if err[0]==2: # "no such file or directory"
                    appuifw.note(u'No serial ports found.','error')
                elif err[0]==4: # "interrupted system call"
                    print "Cancelled by user."
                elif err[0]==13: # "permission denied"
                    print "Discovery failed: permission denied."
                else:
                    raise
                return None
            print "Discovered: %s, %s"%(addr,services)
            if len(services)>1:
                import appuifw
                choices=services.keys()
                choices.sort()
                choice=appuifw.popup_menu([unicode(services[x])+": "+x
                                           for x in choices],u'Choose port:')
                port=services[choices[choice]]
            else:
                port=services[services.keys()[0]]
            address=(addr,port)
            choice=appuifw.query(u'Set as default?','query')
            if choice:
                config['default_target']=address
                # make sure the configuration file exists.
                if not os.path.isdir(CONFIG_DIR):
                    os.makedirs(CONFIG_DIR)
                f=open(CONFIG_FILE,'wt')
                f.write(repr(config))
                f.close()
                
    print "Connecting to "+str(address)+"...",
    try:
        sock.connect(address)
    except socket.error, err:
        if err[0]==54: # "connection refused"
            appuifw.note(u'Connection refused.','error')
            return None
        raise
    print "OK."
    return sock

class _printer:
    def __init__(self,message):
        self.message=message
    def __repr__(self):
        return self.message
    def __call__(self):
        print self.message

_commandhelp=_printer('''Commands:
    Backspace   erase
    C-p, C-n    command history prev/next
    C-u         discard current line
    C-d         quit

If the Pyrepl for Series 60 library is installed, you can start the
full featured Pyrepl line editor by typing "pr()".

execfile commands for scripts found in /system/apps/python/my are
automatically entered into the history at startup, so you can run a
script directly by just selecting the start command with ctrl-p/n.''')

try:
    import sync
except:
    # add 'my' directory into the path, just in case sync.py is only there
    if not filter( lambda p: os.path.split(p)[1] == 'my', sys.path ):
        sethome()
        sys.path.append( os.path.join( HOME, 'my' ) )
    try:
        import sync
    except:
        print "Can't find the sync module"

def synchronize( with_reload=False ):
    sync.main( False, with_reload=with_reload )

HOME = ''
def sethome():            
    global HOME
    if not HOME:
        if os.path.exists('c:/system/apps/python/python.app'):
            HOME = 'c:/system/apps/python'
        elif os.path.exists('e:/system/apps/python/python.app'):
            HOME = 'e:/system/apps/python'

def gohome():
    sethome()
    os.chdir( HOME )    


class myexec:
    ''' This class attaches itself into repr and intercepts the command
    line before it is executed.
    It allows us to create a simple shell.
    '''
    def __init__( self, orig_exec ):
        self.orig_exec = orig_exec
    def __call__( self, text ):
        # l[0] will contain command, l[1] the rest
        l = text.split(' ', 1)
        if len(l) == 2:
            l[1] = l[1].strip()
        else:
            l.append( '' )
        if l[0] == 'ls':
            try:
                # get the files
                if l[1]:
                    files = glob.glob( l[1] )
                    # TODO: add -l mode
                else:
                    files = os.listdir( '.' )
                if files:
                    # add a slash to all directories
                    for i in range(len(files)):
                        if os.path.isdir(files[i]):
                            files[i] += '/'
                    # how wide columns?
                    n = max( [ len(f) for f in files ] ) + 2
                    # how many columns?
                    cols = 80 / n
                    # how many rows per column?
                    rows = [len(files) / cols] * cols
                    # some of the rows get one more
                    for i in range( len(files) % cols ):
                        rows[i] += 1
                    files.extend( ['']*cols )
                    # now figure out how wide columns have to be
                    offset = 0
                    colwidth = []
                    for j in range(cols):
                        try:
                            n = max( [len(f) for f in files[offset:offset+rows[j]]] )
                        except:
                            n = 0
                        colwidth.append( n+1 )
                        offset += rows[j]
                    # print one row at a time
                    for i in range( rows[0] ):
                        # one column at time
                        offset = 0
                        for j in range(cols):
                            print files[i+offset].ljust(colwidth[j]),
                            offset += rows[j]
                        print 
                    print
                else:
                    print '<empty directory>' 
            except:
                import traceback
                traceback.print_exc()
                print 'ls failed'
        elif l[0] == 'cd':
            try:
                if l[1]:
                    os.chdir( l[1] )
                else:
                    gohome()
                print 'current directory:', os.getcwd()
                print
                self.__call__( 'ls' )
            except:
                import traceback
                traceback.print_exc()
                print 'cd failed'
        elif l[0] == 'rm':
            try:
                if os.path.isfile( l[1] ):
                    os.unlink( l[1] )
                elif os.path.isdir( l[1] ):
                    if os.listdir( l[1] ):
                        print 'can only delete empty directories'
                    else:
                        # it's an empty directory
                        os.rmdir( l[1] )
            except:
                print 'rm failed'
        elif l[0] in ['less', 'more', 'cat']:
            try:
                if os.path.isfile( l[1] ):
                    print open( l[1], 'r' ).read()
                else:
                    print '"%s" is not a file' % l[1]
            except:
                print 'rm failed'
        elif l[0] == 'pwd':
            print os.getcwd()
        elif l[0] == 'sync':
            synchronize( with_reload = False )
        elif l[0] == 'syncl':
            synchronize( with_reload = True )
        else:
            self.orig_exec( text )

def pr( names=None ):    
    print 'Starting Pyrepl.'
    try:
        import pyrepl.socket_console
        import pyrepl.python_reader
    except ImportError:
        print "Pyrepl for Series 60 library is not installed."
        return False
    if names is None:
        names = locals()
    print 'Type "prhelp()" for a list of commands.'
    socketconsole = pyrepl.socket_console.SocketConsole(sys.stdin.socket)
    readerconsole = pyrepl.python_reader.ReaderConsole(socketconsole,names)
    readerconsole.run_user_init_file()
    readerconsole.execute = myexec( readerconsole.execute )
    readerconsole.interact()
    return True

STARTUPFILE='c:\\startup.py'

DEFAULT_BANNER='''Python %s on %s
Type "copyright", "credits" or "license" for more information.
Type "commands" to see the commands available in this simple line editor.''' % (sys.version, sys.platform)

def interact(banner=None,readfunc=None,names=None):
    """Thin wrapper around code.interact that will
    - load a startup file ("""+STARTUPFILE+""") if it exists.
    - add the scripts in script directories to command history, if
      the standard input has the .history attribute.
    - call code.interact
    - all exceptions are trapped and all except SystemExit are re-raised."""
    if names is None:
        names=locals()
    if readfunc is None:
        readfunc=_readfunc
    names.update({'pr': lambda: pr(names),
                  'sync': lambda: synchronize( with_reload=False ),
                  'syncl': lambda: synchronize( with_reload=True ),
                  'commands': _commandhelp,
                  'exit': 'Press Ctrl-D on an empty line to exit',
                  'quit': 'Press Ctrl-D on an empty line to exit'})
    if os.path.exists(STARTUPFILE):
        print "Running %s..."%STARTUPFILE
        execfile(STARTUPFILE,globals(),names)
    if hasattr(sys.stdin,"history"):
        # Add into command history the start commands for Python scripts
        # found in these directories.
        PYTHONDIRS=['c:\\system\\apps\\python\\my','e:\\system\\apps\\python\\my']
        for k in PYTHONDIRS:
            if os.path.isdir(k):
                sys.stdin.history+=["execfile("+repr(os.path.join(k,x))+")"
                                    for x in os.listdir(k)
                                    if x.endswith('.py')]            
    try:
        if not pr( names ):
            import code
            # If banner is None, code.interact would print its' own default
            # banner. In that case it makes sense for us to print our help.
            if banner is None:
                banner=DEFAULT_BANNER
            code.interact(banner,readfunc,names)
    except SystemExit:
        print "SystemExit raised."
    except:
        print "Interpreter threw an exception:"
        import traceback
        traceback.print_exc()
        raise
    print "Interactive interpreter finished."


def run_with_redirected_io(sock, func, *args, **kwargs):
    """Call func with sys.stdin, sys.stdout and sys.stderr redirected
    to the given socket, using a wrapper that implements a rudimentary
    line editor with command history. The streams are restored when
    the function exits. If this function is called from the UI thread,
    an exit key handler is installed for the duration of func's
    execution to close the socket.

    Any extra arguments are passed to the function. Return whatever
    the function returns."""
    # Redirect input and output to the socket.
    sockio=socket_stdio(sock)        
    real_io=(sys.stdin,sys.stdout,sys.stderr)
    real_rawinput=__builtins__['raw_input']
    if e32.is_ui_thread():
        import appuifw
        old_exit_key_handler=appuifw.app.exit_key_handler
        def my_exit_key_handler():
            # The standard output and standard error are redirected to
            # previous values already at this point so that we don't
            # miss any messages that the dying program may print.
            sys.stdout=real_io[1]
            sys.stderr=real_io[2]
            # Shutdown the socket to end any reads from stdin and make
            # them raise an EOFError.
            sock.shutdown(2)
            sock.close()
            if e32.is_ui_thread():
                appuifw.app.exit_key_handler=old_exit_key_handler
        appuifw.app.exit_key_handler=my_exit_key_handler
    try:
        (sys.stdin,sys.stdout,sys.stderr)=(sockio,sockio,sockio)
        # Replace the Python raw_input implementation with our
        # own. For some reason the built-in raw_input doesn't flush
        # the output stream after writing the prompt, and The Powers
        # That Be refuse to fix this. See Python bug 526382.
        __builtins__['raw_input']=_readfunc
        return func(*args,**kwargs)
    finally:
        (sys.stdin,sys.stdout,sys.stderr)=real_io
        __builtins__['raw_input']=real_rawinput
        if e32.is_ui_thread():
            appuifw.app.exit_key_handler=old_exit_key_handler

def inside_btconsole():
    return isinstance(sys.stdout,socket_stdio)

def run(banner=None,names=None):
    """Connect to a remote host via Bluetooth and run an interactive
    console over that connection. If sys.stdout is already connected
    to a Bluetooth console instance, use that connection instead of
    starting a new one. If names is given, use that as the local
    namespace, otherwise use namespace of module __main__."""
    if names is None:
        import __main__
        names=__main__.__dict__
    if inside_btconsole():
        # already inside btconsole, no point in connecting again.
        interact(banner,None,names)
    else:
        sock=None
        try:            
            sock=connect()
            if sock is None:
                print "Connection failed."
                return
            sock.send('\r\nConnected.\r\n')
            run_with_redirected_io(sock,interact,banner,None,names)
        finally:
            if sock: sock.close()

def main():
    """The same as execfile()ing the btconsole.py script. Set the
    application title and run run() with the default banner."""
    if e32.is_ui_thread():
        import appuifw
        old_app_title=appuifw.app.title
        appuifw.app.title=u'BTConsole'
    try:
        gohome()
        run()
    finally:
        if e32.is_ui_thread():
            appuifw.app.title=old_app_title
    print "Done."

__all__=['connect','run','interact','main','run_with_redirected_io',
         'inside_btconsole']

if __name__ == "__main__":
    main()
