#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-

#  PycaWM
#  pycarepl
#  Copyright (c) 2007-2008 Vincent Rasneur, Anaël Verrier

#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; version 3 only.

#  This program 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 General Public License for more details.

#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

import sys

if sys.version_info < (2, 5):
    raise ImportError('pycarepl needs at least Python 2.5!')

import os
import readline
import socket
import struct

from cPickle import loads as pickle_loads
from optparse import OptionParser
from pwd import getpwuid
from re import compile as re_compile
from select import select
from stat import ST_MODE, S_ISSOCK

from pycawm.basedirspec import load_first_config, save_config_path

HISTORY_FILE = 'pycarepl_history'
HISTORY_LENGTH = 50
SOCK_NAME_START = '/tmp/pycawm-'
SOCK_NAME_RE = re_compile(SOCK_NAME_START + '([0-9]+)-(.*)')

class PycaREPL(object):
    def __init__(self, display=None, uid=None):
        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock_name = get_first_socket_name(uid=uid, display=display)
        
        if sock_name is None:
            print 'An error ocurred:'
            print '  * no PycaWM was running',
            if display is not None:
                print 'on display %s' % display
            else:
                print
            print '  * or no RemoteREPL plugin was loaded'
            if uid is not None:
                print '  * or the user id was wrong'
            raise SystemExit(-1)

        try:
            self.sock.connect(sock_name)
        except socket.error:
            print 'You do not have enough privileges to connect!'
            raise SystemExit(-1)

        infos = parse_socket_name(sock_name)
        if infos is not None:
            print ('-> Connected to the PycaWM running on display %s' %
                   infos[1])
        hist_path = load_first_config(HISTORY_FILE)
        if hist_path is None:
            # create the history file
            conf_path = save_config_path()
            hist = open(os.path.join(conf_path, HISTORY_FILE), 'w')
            hist.close()
        else:
            readline.read_history_file(hist_path)
        readline.set_history_length(HISTORY_LENGTH)
        self.matches = list()
        self.prompt = ''
        self.sockets = [self.sock]
        readline.set_completer(self.complete)
        readline.parse_and_bind('tab: complete')

    def recv_data(self):
        # retrieve size of the data
        size = struct.calcsize('I')
        size = self.sock.recv(size)
        # retrieve data
        size = struct.unpack('<I', size)[0]
        data = self.sock.recv(size)
        return data

    def send_data(self, text, type_):
        data_sent = type_ + text
        size = struct.pack('<I', len(data_sent))
        self.sock.sendall(size + data_sent)

    def global_matches(self, text):
        self.send_data(text, 'G')

    def attr_matches(self, text):
        self.send_data(text, 'A')

    def complete(self, text, state):
        if state == 0:
            self.sockets.remove(self.sock)
            if '.' in text:
                self.attr_matches(text)
                data = self.recv_data()
                self.matches = pickle_loads(data[1:])
            else:
                self.global_matches(text)
                data = self.recv_data()
                self.matches = pickle_loads(data[1:])
            self.sockets.append(self.sock)
        try:
            return self.matches[state]
        except IndexError:
            return None

    def loop(self):
        sockets = self.sockets
        while 1:
            while not self.prompt:
                select(sockets, list(), list(), 0.3)
                try:
                    data = self.recv_data()
                except socket.error:
                    # do not crash if the wm sleeps a bit
                    continue
                if data[0] == 'P':
                    self.prompt = data[1:]
                elif data[0] == 'D':
                    print data[1:],
                elif data[0] == 'Q':
                    print '\nQuitting: ' + data[1:],
                    self.quit()
                else:
                    print 'wrong data!\n'
                    print repr(data)
                    self.quit(retcode=-1)
            try:
                reply = raw_input(self.prompt)
                self.prompt = ''
                if not reply:
                    reply = '\n'
                self.send_data(reply, 'R')
            except (EOFError, KeyboardInterrupt, socket.error):
                self.quit()

    def quit(self, retcode=0):
        print '\nYou\'re leaving the PycaWM REPL...'
        hist_path = load_first_config(HISTORY_FILE)
        if hist_path is not None:
            readline.write_history_file(hist_path)
        self.sock.close()
        raise SystemExit(retcode)

class ListPycaWMs(object):
    def __init__(self):
        print 'PycaWMs you can connect to:'
        print '---------------------------'
        self.list_pycawms()

    @classmethod
    def list_pycawms(cls):
        socks_name = list(get_sockets_name())
        user_socks_infos = list()
        other_socks_infos = list()
        sock_base = '%s%s-' % (SOCK_NAME_START, os.getuid())
        for sock_name in socks_name:
            sock_infos = parse_socket_name(sock_name)
            if sock_name.startswith(sock_base):
                user_socks_infos.append(sock_infos)
            else:
                other_socks_infos.append(sock_infos)
        user_socks_infos.sort(cmp=cls.cmp_socket_infos)
        other_socks_infos.sort(cmp=cls.cmp_socket_infos)
        print '- Running under the same uid as yours:'
        cls.list_sockets(user_socks_infos)
        print '- Running under other uids:'
        cls.list_sockets(other_socks_infos)

    @staticmethod
    def cmp_socket_infos(x, y):
        # check for not well parsed socket infos
        if x is None and y is None:
            return 0
        # x is good, y is bad
        if y is None:
            return 1
        # x is bad, y is good
        if x is None:
            return -1
        # different uids?
        if x[0] != y[0]:
            return cmp(x[0], y[0])
        # compare the display names if all else is equal
        return cmp(x[1], y[1])

    @classmethod
    def list_sockets(cls, socks_infos):
        if socks_infos:
            for sock_infos in socks_infos:
                cls.list_socket(sock_infos)
        else:
            print '  No PycaWM running!'

    @staticmethod
    def list_socket(sock_infos):
        print '  * PycaWM running',
        if sock_infos:
            uid = sock_infos[0]
            try:
                user = getpwuid(uid)[0]
            except KeyError:
                user = '?'
            display = sock_infos[1]
            print ('under uid %s (user %s) on display %s' %
                   (uid, user, display))
        else:
            print '(parsing error!)'

def get_sockets_name(uid=None, display=None):
    sock_dir = os.path.dirname(SOCK_NAME_START)
    if uid is not None:
        sock_base = '%s%s-' % (SOCK_NAME_START, uid)
    else:
        sock_base = SOCK_NAME_START
    for sock_name in map(lambda n: os.path.join(sock_dir, n),
                         os.listdir(sock_dir)):
        if (sock_name.startswith(sock_base) and
            (display is None or sock_name.endswith(display)) and
            S_ISSOCK(os.stat(sock_name)[ST_MODE])):
            yield sock_name

def get_first_socket_name(uid=None, display=None):
    for sock_name in get_sockets_name(uid=uid, display=display):
        return sock_name
    return None

def parse_socket_name(sock_name):
    infos = SOCK_NAME_RE.match(sock_name)
    if infos is not None:
        return (int(infos.group(1)), infos.group(2))
    return None

def show_title():
    print '/---------------------------------------------------------------\\'
    print '| PycaWM Copyright (C) 2007-2008 Vincent Rasneur, Anaël Verrier |'
    print '| This program comes with ABSOLUTELY NO WARRANTY!               |'
    print '| This is free software, and you are welcome to redistribute it |'
    print '| under certain conditions; see the COPYING file for details.   |'
    print '\\---------------------------------------------------------------/'


if __name__ == '__main__':
    parser = OptionParser()
    parser.set_description('Use pycarepl to have access to a PycaWM '
                           'remote REPL.\n')
    parser.add_option('-q', '--quiet', action='store_true', default=False,
                      help='do not show the title screen')
    parser.add_option('-d', '--display', action='store', type='string',
                      help='connect to the PycaWM running on display DISPLAY')
    parser.add_option('-u', '--uid', action='store', type='int',
                      help='uid of the PycaWM you want to connect to')
    parser.add_option('-l', '--list', action='store_true', default=False,
                      help='list all the PycaWMs you can connect to')
    options, _ = parser.parse_args()
    if options.list:
        ListPycaWMs()
    else:
        if not options.quiet:
            show_title()
        PycaREPL(display=options.display, uid=options.uid).loop()
