#!/usr/bin/python
# - LICENSE HEADER --------------------------------------------------------- {{{
#
# (c) 2008, Bernhard Walle <bwalle@suse.de>
#
# 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, either version 2 of the License, or
# (at your option) any later version.
#
# 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, see <http://www.gnu.org/licenses/>.
#

# }}}
# - Imports ---------------------------------------------------------------- {{{
import socket
import ConfigParser
import os
import readline
import atexit
import logging
import base64
import getpass
import crypt
import pwd
import sys
import termios, struct, fcntl
import hashlib
try:
    import subprocess
except ImportError, e:
    sys.exit('Your Python is too old. You need Python 2.4')
from optparse import OptionParser
try:
    import hashlib
    use_external_hashlib = False
except ImportError:
    try:
        subprocess.Popen(['openssl', '-h'],stderr=subprocess.PIPE).communicate()
        use_external_hashlib = True
    except OSError:
        print 'openssl is not available. '+ \
            'Upgrade to Python 2.5 or install openssl.'
        sys.exit(1)

# }}}
# - Global variables ------------------------------------------------------- {{{
VERSION = '0.6.2'
PACKAGE = 'orthos ' + VERSION
ALIASES = {
    'allmachines'  : 'QUERY name, cpu_cores, ram, res_by, rpower, sconsole, '
                     'status_ping, status_login',
    'mymachines'   : 'QUERY name, cpu_cores, ram, res_by, rpower, sconsole, '
                     'status_ping, status_login WHERE res_by = $USERNAME',
    'freemachines' : 'QUERY name, cpu_cores, ram, rpower, sconsole, '
                     'status_ping, status_login WHERE NOT reserved',
    'lspci'        : 'QUERY pci_slot, pci_vendorid, pci_vendorname, pci_deviceid, '+
                     'pci_devicename, pci_svendorid, '+
                     'pci_svendorname, pci_sdeviceid, '+
                     'pci_sdevicename, pci_revision, pci_driver where name = ',
    'platforms'    : 'QUERY plat_domain, plat_vendor, plat_desc, plat_info, '+
                     'plat_casetype, plat_caseheight',
    'daily_checks' : 'QUERY check_login, check_ssh, check_login, '+
                     ' check_autobuild, check_sysinfo where name = '
}

# }}}
# - Global functions ------------------------------------------------------- {{{
def calc_sha512_hash(string):
    if not use_external_hashlib:
        logging.debug('Using internal SHA512 implementation.')
        hash = hashlib.sha512()
        hash.update(string)
        return hash.digest()
    else:
        logging.debug('Using external openssl SHA512 implementation.')
        p = subprocess.Popen(['openssl', 'dgst', '-sha512', '-binary'],
                stdout=subprocess.PIPE, stdin=subprocess.PIPE)
        p.stdin.write(string)
        return p.communicate()[0]

# }}}
# - InputLineReader -------------------------------------------------------- {{{
class OrthosLineReader(object):

    def __init__(self, orthos, history=None):
        self.orthos = orthos
        self.prompt = ''
        self.completion = True

        try:
            if history:
                readline.read_history_file(history)
        except IOError, e:
            pass

        if history:
            atexit.register(readline.write_history_file, history)

        readline.set_completer_delims(' ')
        readline.set_completer(self.orthos_completer)
        readline.parse_and_bind('tab: complete')

    def disable_completion(self):
        self.completion = False

    def enable_completion(self):
        self.completion = True

    def orthos_completer(self, text, state):
        if not self.completion:
            return None

        try:
            self.orthos.send_server_command_nocheck('COMPLETE', state, text)
            reply  = self.orthos.get_next_server_reply()
            if reply[0] != 'COMPLETERESULT':
                print 'Invalid response.'
                return None
            else:
                if not reply or len(reply) == 1:
                    return None
                return reply[1]
        except Exception, e:
            print e

    def hook(self):
        readline.insert_text(self.default)
        readline.redisplay()

    def set_prompt(self, prompt):
        self.prompt = prompt

    def readline(self, prompt=None, default=None, history=True, complete=True):
        old_completion = self.completion
        if not complete:
            self.disable_completion()

        prompt_suffix = ''
        if default:
            try:
                readline.set_pre_input_hook(self.hook)
            except AttributeError:
                prompt_suffix = ' [' + str(default) + ']'
            self.default = default
        if not prompt:
            prompt = self.prompt
        prompt += prompt_suffix
        if len(prompt) > 0:
            prompt += ' '

        try:
            result = raw_input(prompt)
            if not history and len(result) > 0:
                readline.remove_history_item( \
                    readline.get_current_history_length()-1)
        finally:
            if default:
                try:
                    readline.set_pre_input_hook(None)
                except AttributeError:
                    pass
            if not complete and old_completion:
                self.enable_completion()

        return result

# }}}
# - EncodeException -------------------------------------------------------- {{{
class EncodeException(Exception):
    pass

# }}}
# - Tokenizer -------------------------------------------------------------- {{{
class Tokenizer:
    """
    Simple string tokenizer.
    """
    STATE_BEGIN = 0
    STATE_NORMAL = 1
    STATE_QUOTES = 2
    STATE_SINGLEQUOTE = 3
    STATE_ESCAPE = 4
    STATE_ESCAPE_SINGLE = 5

    def __init__(self):
        """Constructor."""
        self.__list = []
        self.__state = Tokenizer.STATE_BEGIN
        self.__current_str = ''

    def feed(self, string):
        """Feeds the tokeniser with new tokens"""

        if not string:
            return

        for char in string:
            if self.__state == Tokenizer.STATE_BEGIN:
                if char == '"':
                    self.__state = Tokenizer.STATE_QUOTES
                elif char == '\'':
                    self.__state = Tokenizer.STATE_SINGLEQUOTE
                elif char != '\t' and char != ' ':
                    self.__current_str += char
                    self.__state = Tokenizer.STATE_NORMAL
            elif self.__state == Tokenizer.STATE_SINGLEQUOTE:
                if char == '\\':
                    self.__state = Tokenizer.STATE_ESCAPE_SINGLE
                elif char == '\'':
                    self.__list.append(self.__current_str)
                    self.__current_str = ''
                    self.__state = Tokenizer.STATE_BEGIN
                else:
                    self.__current_str += char
            elif self.__state == Tokenizer.STATE_NORMAL:
                if char == '\t' or char == ' ':
                    self.__list.append(self.__current_str)
                    self.__current_str = ''
                    self.__state = Tokenizer.STATE_BEGIN
                else:
                    self.__current_str += char
            elif self.__state == Tokenizer.STATE_QUOTES:
                if char == '\\':
                    self.__state = Tokenizer.STATE_ESCAPE
                elif char == '"':
                    self.__list.append(self.__current_str)
                    self.__current_str = ''
                    self.__state = Tokenizer.STATE_BEGIN
                else:
                    self.__current_str += char
            elif self.__state == Tokenizer.STATE_ESCAPE:
                if char == '"':
                    self.__current_str += char
                elif char == 'n':
                    self.__current_str += '\n'
                elif char == '\\':
                    self.__current_str += '\\'
                else:
                    self.__current_str += '\\' + char
                self.__state = Tokenizer.STATE_QUOTES
            elif self.__state == Tokenizer.STATE_ESCAPE_SINGLE:
                if char == '\'':
                    self.__current_str += char
                elif char == 'n':
                    self.__current_str += '\n'
                elif char == '\\':
                    self.__current_str += '\\'
                else:
                    self.__current_str += '\\' + char
                self.__state = Tokenizer.STATE_SINGLEQUOTE
            else:
                raise ProgramError('Invalid state')

        if len(self.__current_str) > 0:
            self.__list.append(self.__current_str)
        self.__current_str = ''
        self.__state = Tokenizer.STATE_BEGIN

    def get_tokens(self):
        return self.__list

# }}}
# - StringEncoder ---------------------------------------------------------- {{{
class PyStringEncoder(object):
    """
    Encodes a string into a syntax that can be transferred to the client.
    """

    def __init__(self):
        """Constructs a new StringEncoder object."""
        pass

    def encode(self, string):
        """Encodes the given string."""
        return str([string])[1:-1]

# }}}
# - ComplexEncoder --------------------------------------------------------- {{{
class PyComplexEncoder(object):
    """
    Encodes complex types (lists and dictionaries).
    """

    def __init__(self):
        """Constructs a new ComplexEncoder object."""
        pass

    def encode(self, object):
        """
        Encodes the given object. The top-level object must be a list or a
        dictionary. The elements can be either lists or dictionaries itelf or
        primitive types (integer, string, boolens).
        """
        return str(object)

# }}}
# - PyEncoder -------------------------------------------------------------- {{{
class PyEncoder(object):
    """
    Intelligent encoder that automatically chooses the right encoder.
    """

    def __init__(self):
        """Constructs a new PyEncoder object."""
        pass

    def encode(self, object):
        if type(object) == type(''):
            return PyStringEncoder().encode(object)
        elif type(object) == type({}) or type(object) == type([]):
            return PyStringEncoder().encode(PyComplexEncoder().encode(object))
        elif type(object) == type(0) or type(object) == type(0L):
            return str(object)
        else:
            raise EncodeException('Invalid type given: %s' % (type(object)))

# }}}
# - Encoder ---------------------------------------------------------------- {{{
class Encoder(object):
    """
    Encoder class (that should be used by the program).
    """

    PYTHON = 0
    JSON = 1

    def __init__(self, type=PYTHON):
        """Constructs a new Encoder object."""
        if type == Encoder.PYTHON:
            self.encoder = PyEncoder()
        else:
            raise EncodeException('JSON not implemented.')

    def encode(self, object):
        """Encodes the object."""
        return self.encoder.encode(object)

# }}}
# - PyComplexDecoder ------------------------------------------------------- {{{
class PyDecoder(object):
    """
    Decoder class for Python-encoded objects.
    """

    def __init__(self):
        pass

    def decode(self, string):
        """Decodes the object."""
        try:
            exec('x = ' + string)
            return x
        except Exception, e:
            raise EncodeException('Exception occurred while decoding '+
                'string: %s' % (str(e)))

# }}}
# - Decoder ---------------------------------------------------------------- {{{
class Decoder(object):
    """
    Decoder class (that should be used by the program).
    """

    PYTHON = 0
    JSON = 1

    def __init__(self, type=PYTHON):
        """Creates a new Decoder object."""
        if type == Decoder.PYTHON:
            self.decoder = PyDecoder()
        else:
            raise EncodeException('JSON not implemented yet.')

    def decode(self, string):
        """Decodes the object."""
        return self.decoder.decode(string)

# }}}
# - Terminal --------------------------------------------------------------- {{{
class Terminal(object):
    """
    Accesses properties of the terminal.
    The two functions screen_with() and screen_height() are from
    the osc (openSUSE BuildService command line client) code
    (c) Dr. Peter Poeml <poeml@suse.de>
    """

    def __init__(self):
        self.__filtercmd = None
        self.__outputfile = None

    def screen_with(self):
        s = struct.pack('HHHH', 0, 0, 0, 0)
        try:
            x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
        except IOError:
            return 80
        return struct.unpack('HHHH', x)[1]

    def screen_height(self):
        s = struct.pack('HHHH', 0, 0, 0, 0)
        try:
            x = fcntl.ioctl(1, termios.TIOCGWINSZ, s)
        except IOError:
            return 25
        return struct.unpack('HHHH', x)[0]

    def need_pager(self, lines, columns):
        pager_policy =  orthos.config.get_use_pager()
        if pager_policy == 'always':
            return True
        elif pager_policy == 'never':
            return False
        elif pager_policy == 'vertical':
            return self.screen_height() < lines
        elif pager_policy == 'horizontal':
            return self.screen_with() < columns
        else:
            return self.screen_height() < lines or \
                    self.screen_with() < columns

    def reset_output(self):
        self.__filtercmd = None
        self.__outputfile = None

    def set_output_filter(self, cmdlist):
        """For the next output, use the filter command."""
        self.__filtercmd = cmdlist

    def set_output_file(self, filename, append):
        """For next command, use the output file."""
        self.__outputfile = filename
        self.__append = append

    def show_pager(self, text):
        p = subprocess.Popen(orthos.config.get_pager(), shell=True, \
            stdin=subprocess.PIPE)
        p.stdin.write(text)
        p.stdin.close()
        p.wait()

    def show_text_horizontal(self, data):
        if type(data) == type([]):
            text = self.format_data_horizontal(data)
        elif type(data) == type({}):
            text = data['value']
        else:
            print 'Invalid data format'
            return

        self.show(text, lines=None, columns=None)

    def use_query_format_machine(self):
        format = orthos.config.get_query_output_format()
        if format == 'auto':
            if not sys.stdin.isatty() or self.__outputfile or self.__filtercmd:
                return True
            else:
                return False
        elif format == 'machine':
            return True
        else:
            return False

    def show(self, text, lines, columns):
        # calculate lines and columns if not already given
        if not lines or not columns:
            columns = 0
            lines = 0
            for line in text.splitlines():
                lines += 1
                columns = max(columns, len(line))

        if self.__filtercmd:
            p = subprocess.Popen(self.__filtercmd, stdin=subprocess.PIPE)
            p.stdin.write(text)
            p.stdin.close()
            p.wait()
            self.reset_output()
            print
        elif self.__outputfile:
            if self.__append:
                mode = 'a'
            else:
                mode = 'w'
            try:
                f = file(self.__outputfile, mode)
                print >> f, text
                f.close()
            except IOError, e:
                print 'I/O Error: %s' % (str(e))
            self.reset_output()
        else:
            if self.need_pager(lines, columns):
                self.show_pager(text)
            else:
                print text

    def show_text(self, data):
        if len(data) < 2:
            print 'No data available.'
            return
        (text, lines, columns) = self.format_data_vertical(data)
        self.show(text, lines, columns)

    def to_string(self, text):
        if text == None:
            return '-'
        else:
            return str(text)

    def format_data_horizontal(self, data):
        """
        Formats data available in the following format:

           [  { 'name'   :  'name 1',
                'type'   :  'type 1',
                'value'  :  'value 1'   },
              { 'name'   :  'name 1',
                'type'   :  'type 1',
                'value'  : [ { 'name'   :  'name 1',
                               'type'   :  'type 1',
                               'value'  :  'value 1'   },
                               ...                        ] },
                ...
            ]
        """
        return self.format_data_horizontal_fun(data, '', 0)


    def format_data_horizontal_fun(self, data, buffer, offset):

        #
        # get the longest column
        #
        longest_col = 0
        for col in data:
            if not col:
                continue
            longest_col = max(len(col['pretty']), longest_col)
        longest_col += 1

        #
        # start line
        #
        if offset == 0:
            buffer += ''.join(['-' for i in range(0, self.screen_with()-1)])
            buffer += '\n'

        #
        # format
        #
        for col in data:
            if col == None:
                buffer += ''.join(['-' for i in range(0, self.screen_with()-1)])
                buffer += '\n'
                continue

            if len(buffer) > 0 and buffer[-1] == '\n':
                buffer += ''.join([' ' for i in range(0, offset)])
            buffer += (col['pretty']).ljust(longest_col) + ' : '

            # simple values, i.e. not a hash
            if type(col['value']) != type([]):
                firstline = True
                for line in str(col['value']).splitlines():
                    if firstline:
                        buffer += line
                        firstline = False
                    else:
                        buffer += ''.join([' ' \
                            for i in range(0, longest_col+3+offset)])
                    buffer += '\n'
            else:
                buffer = self.format_data_horizontal_fun(
                        col['value'], buffer, longest_col+3+offset)
        #
        # end line
        #
        if offset == 0:
            buffer += ''.join(['-' for i in range(0, self.screen_with()-1)])

        return buffer

    def format_data_vertical(self, data):
        """Formats data available in the following format:

           [ [ { 'name'      : 'field name 1',
                 'short'     : 'short field name 1',
                 'type'      : 'type1'                  },
               { 'name'      : 'field name 2',
                 'short'     : 'short field name 2',
                 'type'      : 'type2'                  } ],
             [ '1st value for field 1',   'value for field 2'  ],
             [ '2nd value for field 1',   'value for field 2'  ],
                ...
             [ 'last value for field 1',  'last value for field 2'  ],
           ]

           Returns a tuple (string, lines, columns)
        """

        machine_readable = self.use_query_format_machine()
        buffer = ''
        total_lines = len(data)
        if not machine_readable:
            total_lines += 3
        total_cols = 0

        #
        # get the maximum length for each column
        #

        # compatibility with old servers
        for column in data[0]:
            if not column.has_key('short'):
                column['short'] = column['name']

        columns = len(data[0])
        collen = []
        for column in data[0]:
            collen.append(len(column['short']))

        for dataset in data[1:]:
            no = 0
            for column in dataset:
                collen[no] = max(collen[no], len(self.to_string(column)))
                no += 1

        #
        # format the header
        #
        total_cols = sum(collen) + 2*len(collen)
        line = ''.join(['-' for i in range(0, total_cols)])
        if not machine_readable:
            buffer += '%s\n ' % (line)
        no = 0
        for column in data[0]:
            if not machine_readable:
                buffer += '%s  ' % (column['short'].ljust(collen[no]))
            else:
                buffer += '%s\t' % (column['short'])
            no += 1
        # remove last 2 spaces
        buffer = buffer[0:-1]
        buffer += '\n'
        no += 1
        if not machine_readable:
            buffer += '%s\n' % (line)

        #
        # format the data
        #
        lastdata = None
        for dataset in data[1:]:
            no = 0
            if not machine_readable:
                buffer += ' '
            dupcol = 0
            for column in dataset:
                if not machine_readable and \
                        lastdata and lastdata[no] == column and dupcol == no \
                        and column and len(self.to_string(column)) > 0:
                    column = '"'
                    dupcol += 1
                if not machine_readable:
                    buffer += '%s  ' % (self.to_string(column).ljust( \
                        collen[no]))
                else:
                    buffer += '%s\t' % (self.to_string(column))
                no += 1
            buffer += '\n'
            lastdata = dataset
        if not machine_readable:
            buffer += '%s' % (line)

        return (buffer, total_lines, total_cols)

# }}}
# - Config ----------------------------------------------------------------- {{{
class Config:
    """
    Configuration
    """

    DEFAULT_SERVERNAME = 'music.arch.suse.de'
    DEFAULT_PORT = 5555
    USER_CONFIGFILE = '~/.orthosrc'
    SYSTEM_CONFIGFILE = '/etc/orthosrc'
    USER_HISTORYFILE = '~/.orthos.history'
    PROMPT = '(orthos %(version)s)'

    def __init__(self):
        self.__cp = ConfigParser.RawConfigParser()
        self.__cp.read(Config.SYSTEM_CONFIGFILE)
        self.__cp.read(os.path.expanduser(Config.USER_CONFIGFILE))
        self.__aliases = {}
        self.__port = None
        self.__server = None
        if not sys.stdin.isatty():
            self.__quiet = True
        else:
            self.__quiet = False
        for alias in ALIASES:
            self.__aliases[alias] = ALIASES[alias]
        if self.__cp.has_section('alias'):
            for key in self.__cp.options('alias'):
                self.__aliases[key.lower()] = self.__cp.get('alias', key)

    def set_quiet(self, quiet):
        self.__quiet = quiet

    def is_quiet(self):
        return self.__quiet

    def set_server(self, server):
        self.__server = server

    def get_server(self):
        if self.__server:
            return self.__server
        else:
            if self.__cp.has_option('global', 'server'):
                return self.__cp.get('global', 'server')
            else:
                return Config.DEFAULT_SERVERNAME

    def set_port(self, port):
        self.__port = port

    def get_port(self):
        if self.__port:
            return self.__port
        else:
            if self.__cp.has_option('global', 'port'):
                return self.__cp.getint('global', 'port')
            else:
                return Config.DEFAULT_PORT

    def get_history(self):
        if self.__cp.has_option('global', 'history'):
            hist = self.__cp.get('global', 'history')
        else:
            hist = Config.USER_HISTORYFILE
        return os.path.expanduser(hist)

    def get_prompt(self):
        if self.__cp.has_option('global', 'prompt'):
            prompt = self.__cp.get('global', 'prompt')
        else:
            prompt = Config.PROMPT
        prompt = prompt % ({ 'version' : VERSION })
        return prompt

    def get_username(self):
        if self.__cp.has_option('global', 'username'):
            return self.__cp.get('global', 'username')
        else:
            return pwd.getpwuid(os.getuid())[0]

    def set_username(self, username):
        if not self.__cp.has_section('global'):
            self.__cp.add_section('global')
        self.__cp.set('global', 'username', username)
        self.write_config()

    def set_pager(self, pager):
        if not self.__cp.has_section('global'):
            self.__cp.add_section('global')
        self.__cp.set('global', 'pager', pager)
        self.write_config()

    def get_pager(self):
        if self.__cp.has_option('global', 'pager'):
            return self.__cp.get('global', 'pager')
        else:
            return 'less -S'

    def set_use_pager(self, use_pager):
        values = ['always', 'never', 'horizontal', 'vertical', 'both']
        if use_pager not in values:
            raise ValueError('use_pager must be in %s' % ', '.join(values))

        if not self.__cp.has_section('global'):
            self.__cp.add_section('global')
        self.__cp.set('global', 'use_pager', use_pager)
        self.write_config()

    def get_use_pager(self):
        if sys.stdin.isatty():
            if self.__cp.has_option('global', 'use_pager'):
                return self.__cp.get('global', 'use_pager')
            else:
                return 'both'
        else:
            return 'never'

    def set_default_domains(self, default_domains):
        # check if values are sane
        try:
            [int(value) for value in default_domains]
        except ValueError:
            raise ValueError('default_domains must set to integer values')

        if not self.__cp.has_section('global'):
            self.__cp.add_section('global')
        self.__cp.set('global', 'default_domains',
                ', '.join(map(lambda x: str(x), default_domains)))
        self.write_config()

    def get_default_domains(self):
        if self.__cp.has_option('global', 'default_domains'):
            string = self.__cp.get('global', 'default_domains')
            return map(lambda x: int(x), string.split(', '))
        else:
            return [0]

    def get_default_domains_string(self):
        domainstr = [str(x) for x in self.get_default_domains()]
        return ' '.join(domainstr)

    def get_query_output_format(self):
        if self.__cp.has_option('global', 'query_output_format'):
            return self.__cp.get('global', 'query_output_format')
        else:
            return 'auto'

    def set_query_output_format(self, query_output_format):
        values = ['auto', 'machine', 'human']
        if query_output_format not in values:
            raise ValueError('query_output_format must be in %s' % \
                ', '.join(values))

        if not self.__cp.has_section('global'):
            self.__cp.add_section('global')
        self.__cp.set('global', 'query_output_format', query_output_format)
        self.write_config()

    def get_aliases(self):
        return self.__aliases

    def set_alias(self, name, value):
        self.__aliases[name] = value
        if not self.__cp.has_section('alias'):
            self.__cp.add_section('alias')
        for alias in self.__aliases:
            self.__cp.set('alias', alias, self.__aliases[alias])
        self.write_config()

    def write_config(self):
        f = file(os.path.expanduser(Config.USER_CONFIGFILE), 'w')
        self.__cp.write(f)
        f.close()

# }}}
# - Orthos ----------------------------------------------------------------- {{{
orthos = None

class Orthos:
    """
    Main class.
    """

    def __init__(self):
        """Constructor of the main class."""
        self.config = Config()
        self.__socket = None
        self.__sfile = None
        self.__terminal = Terminal()
        self.__encoder = Encoder(Encoder.PYTHON)
        self.__decoder = Decoder(Decoder.PYTHON)

    def connect(self, reconnect=False):
        """Connects the socket to the server"""
        serverport = (self.config.get_server(), self.config.get_port())
        if not self.config.is_quiet():
            if reconnect:
                print 'Connection lost, reconnecting ...',
            else:
                print 'Connecting to %s:%d ...' % (serverport),
        sys.stdout.flush()
        self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.__socket.connect(serverport)
        self.__sfile = self.__socket.makefile()
        if reconnect and not self.config.is_quiet():
            print 'Please submit last commands again.'

    def get_next_server_reply(self):
        """
        Gets the next command. Returns a tuple of command and argument or None
        if this was the last command
        """

        line = self.__sfile.readline()
        if not line or len(line) == 0:
            return ('', None)

        line = line.rstrip()

        tok = Tokenizer()
        tok.feed(line)
        tokens = tok.get_tokens()
        logging.debug('Got command %s (%s)' % \
                (tokens[0], " ".join(tokens[1:])))
        return tokens

    def send_server_command(self, command, *args):
        # check the connection
        logging.debug('Sending PING')
        self.__socket.send('PING\n')
        reply = self.get_next_server_reply()
        if len(reply) != 1 or reply[0].upper() != 'PONG':
            self.connect()
            orthos.handshake()

        self.send_server_command_nocheck(command, *args)

    def send_server_command_nocheck(self, command, *args):
        string = ''
        if len(args) > 0:
            string_args = [self.__encoder.encode(x) for x in args]
            string += ' ' + ' '.join(string_args)

        logging.debug('Sending %s %s' % (command, string))
        self.__socket.send(command + ' ' + string + '\n')

    def get_next_user_command(self, prompt=None):
        try:
            input = ''
            while len(input) == 0:
                input = self.__linereader.readline(prompt=prompt)
        except EOFError:
            print ''
            return ('QUIT', None)
        tokenizer = Tokenizer()
        tokenizer.feed(input)
        result = tokenizer.get_tokens()

        # handle alias
        if self.config.get_aliases().has_key(result[0].lower()):
            alias = self.config.get_aliases()[result[0].lower()]
            tokenizer = Tokenizer()
            tokenizer.feed(alias)
            res = tokenizer.get_tokens()
            # replace username
            try:
                res[res.index('$USERNAME')] = self.config.get_username()
            except:
                pass
            res.extend(result[1:])
            result = res

        # handle command line redirection
        if '|' in result:
            idx = result.index('|')
            self.__terminal.set_output_filter(result[idx+1:])
            result = result[0:idx]
        elif '|>' in result:
            idx = result.index('|>')
            self.__terminal.set_output_file(result[idx+1], False)
            result = result[0:idx]
        elif '|>>' in result:
            idx = result.index('|>>')
            self.__terminal.set_output_file(result[idx+1], True)
            result = result[0:idx]

        return result

    def handshake(self):
        self.send_server_command_nocheck('HELLO', VERSION)
        (reply, version)  = self.get_next_server_reply()
        self.__version = version
        if not self.config.is_quiet():
            print 'finished'
        self.set_default_domains_from_config(print_result=False)

        if not self.config.is_quiet():
            domains = self.config.get_default_domains()
            if len(domains) > 0 and not (len(domains) == 1 and domains[0] == 0):
                print ''
                print 'INFO: DEFAULT_DOMAINS is set to ' + \
                    self.config.get_default_domains_string() + '.'
                print '      That means that you don\'t get all machines with QUERY.'
                print
            pass
        pass

    def print_ok_error(self, reply):
        if reply[0].upper() == 'OK':
            if len(reply) > 1:
                print reply[1]
            else:
                print 'OK'
        elif len(reply) > 1 and reply[0].upper() == 'ERROR':
            print 'Error: %s' % (reply[1])
        else:
            print 'Invalid reply.'

    def print_status(self, reply):
        if len(reply) > 1 and reply[0].upper() == 'STATUS':
            print ' - %s' % (reply[1])
        else:
            print 'Invalid reply.'

    def setup_command(self, string):
        self.send_server_command('SETUP', *string)
        while True:
            reply = self.get_next_server_reply()
            if reply[0] == 'AUTH':
                self.auth(reply[1:])
                continue
            elif reply[0] == 'OK' or reply[0] == 'ERROR':
                self.print_ok_error(reply)
                break

    def power_command(self, string):
        self.send_server_command('POWER', *string)
        while True:
            reply = self.get_next_server_reply()
            if reply[0] == 'AUTH':
                self.auth(reply[1:])
                continue
            elif reply[0] == 'OK' or reply[0] == 'ERROR':
                self.print_ok_error(reply)
                break

    def handle_confirm(self, list):
        if len(list) != 2:
            raise ProgramError('CONFIRMREQ takes 2 arguments.')

        msg = list[0]
        default = list[1]
        if default.lower() == 'y':
            firstchar = 'Y'
            secondchar = 'n'
        else:
            firstchar = 'y'
            secondchar = 'N'
        yesno = '%s/%s' % (firstchar, secondchar)

        input = self.__linereader.readline(prompt='%s [%s] ' % (msg, yesno),
                history=False, complete=False)
        if len(input) == 0:
            result = default.lower() == 'y'
        else:
            result = input.lower() == 'y' or input.lower() == 'j'

        if result:
            self.send_server_command_nocheck('CONFIRM', 'y')
        else:
            self.send_server_command_nocheck('CONFIRM', 'n')

    def handle_input(self, list):
        if len(list) < 3:
            raise ProgramError('Invalid input.')
        name = list[0]
        type = list[1]
        default = list[2]
        if type == 'enum':
            enumvalues = self.__decoder.decode(list[3])
            defnum = -1
            for number in range(0, len(enumvalues)):
                if (len(default) > 0 and default == enumvalues[number]) or \
                        len(enumvalues) == 1:
                    defchar = '+'
                    defnum = number
                else:
                    defchar = ' '
                print ' %s (%2d) %s' % (defchar, number, enumvalues[number])
            while True:
                try:
                    input = self.__linereader.readline(prompt=name+'>', \
                        history=False, complete=False)
                    if len(input) == 0 and defnum != -1:
                        input = defnum
                    if int(input) >= 0 and int(input) < len(enumvalues):
                        break
                    else:
                        print 'Invalid input. Try again.'
                except ValueError:
                    print 'Invalid input. Try again.'
            self.send_server_command_nocheck('INPUT', enumvalues[int(input)])
        else:
            input = self.__linereader.readline(prompt=name+'>', \
                    default=default, history=False, complete=False)
            self.send_server_command_nocheck('INPUT', input)

    def auth(self, args):
        timestamp = args[0]
        password = getpass.getpass('Orthos password for %s: ' \
                % self.config.get_username())

        password = hashlib.md5(password).hexdigest()

        enc_password = calc_sha512_hash(timestamp + password)
        enc_password = base64.standard_b64encode(enc_password)

        self.send_server_command_nocheck('AUTH', self.config.get_username(), \
            enc_password)

    def reserve_release_command(self, args, release):
        if release:
            cmd = 'RELEASE'
        else:
            cmd = 'RESERVE'
        self.send_server_command(cmd, *args)
        while True:
            reply = self.get_next_server_reply()
            if reply[0] == 'AUTH' and len(reply) == 2:
                self.auth(reply[1:])
                continue
            elif reply[0] == 'INPUTREQ':
                self.handle_input(reply[1:])
                continue
            elif reply[0] == 'CONFIRMREQ':
                self.handle_confirm(reply[1:])
                continue
            elif reply[0] == 'OK' or reply[0] == 'ERROR':
                self.print_ok_error(reply)
                return
            else:
                print 'Invalid command received: %s' % (reply[0])
                return

    def reserve_history_command(self, args):
        self.send_server_command('RESERVEHISTORY', *args)
	reply = self.get_next_server_reply()
        if reply[0] == 'ERROR':
            self.print_ok_error(reply)
	else:
	    res_list_raw = reply[1]
	    res_list_raw = res_list_raw.replace("None", "'None'")
	    res_list_raw = res_list_raw.lstrip("[")
	    res_list_raw = res_list_raw.rstrip("]")
	    res_list_raw = res_list_raw.lstrip("{")
	    res_list_raw = res_list_raw.rstrip("}")
	    res_list_raw = res_list_raw.split("}, {")

	    res_list = []

	    for row in res_list_raw:
		row_list = row.split(", '")
		kvp_dict = {}
		for rl_row in row_list:
		    rl_row = rl_row.split("' ")
		    for kv_pair in rl_row:
			kv_pair = kv_pair.replace("'", "")
			kvp_list = kv_pair.split(": ")
			kvp_dict[kvp_list[0]] = kvp_list[1]
		res_list.append(kvp_dict)

	    print
	    print '%-15s %-20s %-20s %-20s' % ("Reserved by", "Reserved at", "Reserved until", "Reason")
	    print '================================================================================'

	    for rows in res_list:
		if 'r_by' in rows:
		    print '%-15s' % (rows['r_by']),
		if 'r_at' in rows:
		    print '%-20s' % (rows['r_at']),
		if 'r_until' in rows:
		    print '%-20s' % (rows['r_until']),
		if 'r_reason' in rows:
		    print '%-20s' % (rows['r_reason']),
		print
	    print

	    return
	
    def regenerate_command(self, string):
        self.send_server_command('REGENERATE', *string)
        reply = self.get_next_server_reply()
        self.print_ok_error(reply)

    def synchronize_command(self):
        self.send_server_command('SYNCHRONIZE')
        reply = self.get_next_server_reply()
        self.print_ok_error(reply)

    def query_command(self, args):
        self.send_server_command('QUERY', *args)
        reply = self.get_next_server_reply()
        if reply[0] == 'ERROR':
            self.print_ok_error(reply)
        elif reply[0] == 'QUERYRESULT' and len(reply) == 2:
            try:
                result = self.__decoder.decode(reply[1])
                self.__terminal.show_text(result)
            except EncodeException, e:
                print 'Invalid result.'
        else:
            print 'Invalid reply to QUERY.'

    def rescan_command(self, args):
        self.send_server_command('RESCAN', *args)
        reply = self.get_next_server_reply()
        self.print_ok_error(reply)

    def alias_command(self, sp):
        if len(sp) == 1:
            name = sp[0]
            try:
                result = self.config.get_aliases()[name.lower()]
                print '%s => "%s"' % (name, result)
            except KeyError:
                print '%s is an invalid alias.' % (name)
        if len(sp) == 0:
            for alias in self.config.get_aliases():
                result = self.config.get_aliases()[alias]
                print '%s => "%s"' % (alias, result)
        else:
            # define a new alias
            name = sp[0]
            value = ' '.join(sp[1:])
            self.config.set_alias(name, value)

    def config_command(self, args):
        self.send_server_command('CONFIG', *args)
        while True:
            reply = self.get_next_server_reply()
            if reply[0] in ['ERROR', 'OK']:
                self.print_ok_error(reply)
                break
            elif reply[0] == 'AUTH':
                self.auth(reply[1:])
                continue
            elif reply[0] == 'INFORESULT':
                try:
                    result = self.__decoder.decode(reply[1])
                    self.__terminal.show_text_horizontal(result)
                except EncodeException, e:
                    print 'Invalid result.'
                break
            pass
        pass

    def info_command(self, args):
        self.send_server_command('INFO', *args)
        reply = self.get_next_server_reply()
        if reply[0] == 'ERROR':
            self.print_ok_error(reply)
        elif reply[0] == 'INFORESULT':
            try:
                result = self.__decoder.decode(reply[1])
                self.__terminal.show_text_horizontal(result)
            except EncodeException, e:
                print 'Invalid result.'

    def raw_command(self, args):
        self.send_server_command_nocheck(args[0].upper(), *args[1:])
        reply = self.get_next_server_reply()
        if reply[0] == 'ERROR' or reply[0] == 'OK':
            self.print_ok_error(reply)
        elif reply[0] == 'AUTH':
            self.auth(reply[1:])
        else:
            print 'Invalid reply: %s' % (' '.join(reply[1:]))

    def help_command(self, args):
        self.send_server_command('HELP', *args)
        reply = self.get_next_server_reply()
        if reply[0] == 'ERROR' or reply[0] == 'OK':
            self.print_ok_error(reply)
        elif reply[0] == 'HELPRESULT' and len(reply) == 2:
            self.__terminal.show(reply[1], lines=None, columns=0)
        else:
            print 'Invalid reply: %s' % (' '.join(reply[1:]))

    def modifyadddel_cmd(self, args, cmd):
        self.send_server_command(cmd, *args)
        while True:
            reply = self.get_next_server_reply()
            if reply[0] == 'AUTH' and len(reply) == 2:
                self.auth(reply[1:])
                continue
            elif reply[0] == 'INPUTREQ':
                self.handle_input(reply[1:])
                continue
            elif reply[0] == 'CONFIRMREQ':
                self.handle_confirm(reply[1:])
                continue
            elif reply[0] == 'OK' or reply[0] == 'ERROR':
                self.print_ok_error(reply)
                return
            elif reply[0] == 'STATUS':
                self.print_status(reply)
                # and do NOT return
            else:
                print 'Invalid command received: %s' % (reply[0])
                return

    def set_command(self, sp):
        if len(sp) > 0:
            if sp[0].upper() == 'USER':
                if len(sp) == 1:
                    print 'USER is set to %d' % (self.config.get_username())
                    return
                self.config.set_username(sp[1])
                print 'USER set to %s' % (sp[1])
            elif sp[0].upper() == 'PAGER':
                if len(sp) == 1:
                    print 'PAGER is set to \'%s\'' % (self.config.get_pager())
                    return
                self.config.set_pager(sp[1])
                print 'PAGER set to %s' % (sp[1])
            elif sp[0].upper() == 'QUERY_OUTPUT_FORMAT':
                if len(sp) == 1:
                    print 'QUERY_OUTPUT_FORMAT is set to %s' % \
                    (self.config.get_query_output_format())
                    return
                self.config.set_query_output_format(sp[1])
                print 'QUERY_OUTPUT_FORMAT set to %s' % (sp[1])
            elif sp[0].upper() == 'USE_PAGER':
                if len(sp) == 1:
                    print 'USE_PAGER is set to %s' % \
                    (self.config.get_use_pager())
                    return
                try:
                    self.config.set_use_pager(sp[1])
                    print 'USE_PAGER set to %s' % (sp[1])
                except ValueError, e:
                    print str(e)
            elif sp[0].upper() == 'DEFAULT_DOMAINS':
                if len(sp) > 1:
                    self.config.set_default_domains(sp[1:])
                    self.set_default_domains_from_config(print_result=True)
                elif len(sp) == 1:
                    print 'DEFAULT_DOMAINS is set to ' + \
                        self.config.get_default_domains_string()
                else:
                    print 'SET DEFAULT_DOMAINS needs at least 1 argument.'
            else:
                print 'Invalid variable: %s' % (sp[0])
        else:
            print 'SET requires at least one argument.'

    def vovo_command(self, args):
        self.send_server_command('VOVO', *args)
        reply = self.get_next_server_reply()
        if reply[0] in ['ERROR','OK']:
            self.print_ok_error(reply)
        elif reply[0] == 'QUERYRESULT' and len(reply) == 2:
            try:
                result = self.__decoder.decode(reply[1])
                self.__terminal.show_text(result)
            except EncodeException, e:
                print 'Invalid result.'
        else:
            print 'Invalid reply to QUERY.'


    def set_default_domains_from_config(self, print_result):
        default_domains = self.config.get_default_domains()
        self.send_server_command_nocheck('SSET', 'DEFAULT_DOMAINS', *default_domains)
        reply = self.get_next_server_reply()
        if print_result:
            self.print_ok_error(reply)

    def run(self):
        self.__linereader = OrthosLineReader(self, \
            history=self.config.get_history())
        if sys.stdin.isatty():
            self.__linereader.set_prompt(self.config.get_prompt())

        while True:
            try:
                input = self.get_next_user_command()

                cmd = input[0].upper()
                args = input[1:]
                if cmd == 'POWER':
                    self.power_command(args)
                elif cmd == 'SETUP':
                    self.setup_command(args)
                elif cmd == 'QUIT' or cmd == 'EXIT':
                    if not self.config.is_quiet():
                        print 'Good bye, have a lot of fun...'
                    return
                elif cmd == 'RESERVE':
                    self.reserve_release_command(args, release=False)
                elif cmd == 'RELEASE':
                    self.reserve_release_command(args, release=True)
                elif cmd == 'RESERVEHISTORY':
                    self.reserve_history_command(args)
                elif cmd == 'REGENERATE':
                    self.regenerate_command(args)
                elif cmd == 'QUERY':
                    self.query_command(args)
                elif cmd == 'RESCAN':
                    self.rescan_command(args)
                elif cmd == 'ALIAS':
                    self.alias_command(args)
                elif cmd == 'SET':
                    self.set_command(args)
                elif cmd == 'INFO':
                    self.info_command(args)
                elif cmd == 'CONFIG':
                    self.config_command(args)
                elif cmd == 'HELP':
                    self.help_command(args)
                elif cmd == 'MODIFY' or \
                        cmd == 'DELETE' or \
                        cmd == 'ADD':
                    self.modifyadddel_cmd(args, cmd)
                elif cmd == 'RAW':
                    self.raw_command(args, )
                elif cmd == 'VOVO':
                    self.vovo_command(args)
                elif cmd == 'SYNCHRONIZE':
                    self.synchronize_command()
                else:
                    print 'Invalid command.'
            except socket.error:
                print
                self.connect()
                orthos.handshake()
            except TypeError, e:
                print e
            except KeyboardInterrupt:
                self.send_server_command_nocheck('ABORT')
                reply = self.get_next_server_reply()
                print


    def disconnect(self):
        if self.__sfile:
            self.__sfile.flush()
        if self.__socket:
            self.__socket.close()
        self.__socket = None

# }}}
# - main() ----------------------------------------------------------------- {{{
def main():
    parser = OptionParser(PACKAGE)
    parser.add_option('-D', '--debug', dest='debug', default=False,
            action='store_true', help='Write debugging output')
    parser.add_option('-L', '--logfile', dest='logfile', default=False,
            help='Use that together with -D to log the debug output in a file'+
            'rather than on the console', metavar='FILE')
    parser.add_option('-H', '--host', dest='host',
            help='Use the hostname specified on the command line instead of '+
            'the one in the config file', metavar='HOST')
    parser.add_option('-P', '--port', dest='port',
            help='Use the port specified on the command line instead of the '+
            'one in the config file', metavar='PORT')
    parser.add_option('-v', '--version', dest='version', default=False,
            action='store_true', help='Print version output')
    parser.add_option('-q', '--quiet', dest='quiet', default=False,
            action='store_true', help='Makes orthos quiet.')
    (options, args) = parser.parse_args()

    global orthos
    orthos = Orthos()

    if options.debug:
        if options.logfile:
            logging.basicConfig(filename=options.logfile,
                level=logging.DEBUG,
                format='%(asctime)s %(levelname)s %(message)s')
        else:
            logging.basicConfig(level=logging.DEBUG,
                format='%(asctime)s %(levelname)s %(message)s')
        logging.info('Debugging enabled')

    if options.port:
        orthos.config.set_port(int(options.port))
    if options.host:
        orthos.config.set_server(options.host)
    if options.quiet:
        orthos.config.set_quiet(options.quiet)

    if options.version:
        print PACKAGE
        sys.exit(0)

    try:
        orthos.connect()
        orthos.handshake()
        orthos.run()
    except Exception, e:
        print str(e)
        if options.debug:
            logging.exception(e)

    orthos.disconnect()

# }}}
# - Main part -------------------------------------------------------------- {{{
if __name__ == '__main__':
    main()

# }}}
# vim: set sw=4 ts=4 et fdm=marker: :collapseFolds=1:
