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

# Copyright (C) 2010-2015 by Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
#
# PyHoca CLI is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# PyHoca CLI 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero 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.
#
# Contributors to the code of this programme:
#    2010 Dick Kniep <dick.kniep@lindix.nl>
#    2010 Jörg Sawatzki <joerg.sawatzki@web.de>

###
### module section
###

import x2go

import sys
import os

PROG_NAME = os.path.basename(sys.argv[0]).replace('.exe', '')
PROG_PID  = os.getpid()

if hasattr(sys, 'frozen') and sys.frozen in ("windows_exe", "console_exe"):
    class Win32_Logging(object):

        softspace = 0
        _fname = os.path.join(os.environ['AppData'], PROG_NAME, '%s.log' % PROG_NAME)
        _file = None

        def __init__(self, filemode='a'):
            self._filemode = filemode

        def write(self, text, **kwargs):
            if self._file is None:
                try:
                    try:
                        os.mkdir(os.path.dirname(self._fname))
                    except:
                        pass
                    self._file = open(self._fname, self._filemode)
                except:
                    pass
            else:
                self._file.write(text)
                self._file.flush()

        def flush(self):
            if self._file is not None:
                self._file.flush()

    sys.stdout = Win32_Logging(filemode='w+')
    sys.stderr = Win32_Logging(filemode='a')
    del Win32_Logging

app = sys.argv[0]
if app.startswith('./'):
    sys.path.insert(0, os.getcwd())
    os.environ['PYTHONX2GO_LOCAL'] = '1'

PROG_OPTIONS = " ".join(sys.argv[1:]).replace("=", " ").split()
try:
    _password_index = PROG_OPTIONS.index('--password')+1
    PROG_OPTIONS[_password_index] = "XXXXXXXX"
except ValueError:
    # ignore if --password option is not specified
    pass
try:
    _broker_password_index = PROG_OPTIONS.index('--broker-password')+1
    PROG_OPTIONS[_broker_password_index] = "XXXXXXXX"
except ValueError:
    # ignore if --broker-password option is not specified
    pass

from x2go import X2GOCLIENT_OS as _X2GOCLIENT_OS
if _X2GOCLIENT_OS in ('Linux', 'Mac'):
    import setproctitle
    setproctitle.setproctitle("%s %s" % (PROG_NAME, " ".join(PROG_OPTIONS)))

import argparse
import paramiko

# Python X2Go provides the current local username (OS independent)
from x2go.defaults import CURRENT_LOCAL_USER as current_user

from x2go.defaults import X2GO_PRINT_ACTIONS
from x2go.defaults import DEFAULT_PDFVIEW_CMD
from x2go.defaults import DEFAULT_PDFSAVE_LOCATION
from x2go.defaults import DEFAULT_PRINTCMD_CMD
from x2go import BACKENDS

from pyhoca.cli import current_home, PyHocaCLI, runtime_error

# version information
from pyhoca.cli import __VERSION__ as _version
VERSION=_version
VERSION_TEXT="""
%s[%s] - an X2Go command line client written in Python
----------------------------------------------------------------------
developed by Mike Gabriel <m.gabriel@das-netzwerkteam.de>

VERSION: %s

""" % (PROG_NAME, PROG_PID, VERSION)

PRINT_ACTIONS = X2GO_PRINT_ACTIONS.keys()

logger = x2go.X2GoLogger()
liblogger = x2go.X2GoLogger()

###
### command line arguments
###

# exclusive client control options
action_options =   [
                   {'args':['-N','--new'], 'default': False, 'action': 'store_true', 'help': 'start a new X2Go session on server (default)', },
                   {'args':['-R','--resume'], 'default': None, 'metavar': 'SESSION_NAME', 'help': 'resume a suspended X2Go session with name SESSION_NAME', },
                   {'args':['-D','--share-desktop'], 'default': None, 'metavar': 'USER@DISPLAY', 'help': 'share an X2Go session on server specified by USER@DISPLAY', },
                   {'args':['-S','--suspend'], 'default': None, 'metavar': 'SESSION_NAME', 'help': 'suspend running X2Go session SESSION_NAME', },
                   {'args':['-T','--terminate'], 'default': None, 'metavar': 'SESSION_NAME', 'help': 'terminate running X2Go session SESSION_NAME', },
                   {'args':['-L','--list-sessions'], 'default': False, 'action': 'store_true', 'help': 'list user\'s X2Go sessions on server', },
                   {'args':['--list-desktops'], 'default': False, 'action': 'store_true', 'help': 'list X2Go desktop sessions that are available for sharing', },
                   {'args':['-l','--list-profiles'], 'default': False, 'action': 'store_true', 'help': 'list user\'s X2Go pre-configured session profiles', },
                   {'args':['-P','--session-profile'], 'default': None, 'help': 'load x2goclient session profiles and use the session profile SESSION_PROFILE', },
                 ]
if _X2GOCLIENT_OS == "Linux":
    action_options.append(
                   {'args':['--from-stdin'], 'default': False, 'action': 'store_true', 'help': 'for LightDM remote login: read <username> <password> <host[:port]> <desktopshell> from STDIN', },
    )

# debug options...
debug_options =   [
                    {'args':['-d','--debug'], 'default': False, 'action': 'store_true', 'help': 'enable application debugging code', },
                    {'args':['--quiet'], 'default': False, 'action': 'store_true', 'help': 'disable any kind of log output', },
                    {'args':['--libdebug'], 'default': False, 'action': 'store_true', 'help': 'enable debugging code of the underlying Python X2Go module', },
                    {'args':['--libdebug-sftpxfer'], 'default': False, 'action': 'store_true', 'help': 'enable debugging code of Python X2Go\'s sFTP server code (very verbose, and even promiscuous)', },
                    {'args':['-V', '--version'], 'default': False, 'action': 'store_true', 'help': 'print version number and exit', },
                  ]
# possible programme options are
x2go_options =    [
                    # NOT IMPLEMENTED {'args':['--config'], 'default': '~/.x2goclient/sessions', 'help': 'x2goclient config file containing x2go session settings (default: ~/.x2goclient/sessions)', },
                    {'args':['-c','--command'], 'default': 'TERMINAL', 'help': 'command to run with -R mode on server (default: xterm)', },
                    {'args':['-u','--username'], 'default': None, 'help': 'username for the session (default: current user)', },
                    {'args':['--password'], 'default': None, 'help': 'user password for session authentication', },
                    {'args':['-p','--remote-ssh-port'], 'default': '22', 'help': 'remote SSH port (default: 22)', },
                    {'args':['-k','--ssh-privkey'], 'default': None, 'help': 'use file \'SSH_PRIVKEY\' as private key for the SSH connection (e.g. ~/.ssh/id_rsa)', },
                    {'args':['--add-to-known-hosts'], 'default': False, 'action': 'store_true', 'help': 'add RSA host key fingerprint to ~/.ssh/known_hosts if authenticity of server can\'t be established (default: not set)', },
                    {'args':['--sound'], 'default': 'pulse', 'choices': ('pulse', 'esd', 'none'), 'help': 'X2Go server sound system (default: \'pulse\')', },
                    {'args':['--printing'], 'default': False, 'action': 'store_true', 'help': 'use X2Go printing (default: disabled)', },
                    {'args':['--share-mode'], 'default': 0, 'help': 'share mode for X2Go desktop sharing (0: view-only, 1: full access)', },
                    {'args':['-F', '--share-local-folders'], 'metavar': '<folder1>[,<folder2[,...]]', 'default': None, 'help': 'a comma separated list of local folder names to mount in the X2Go session', },
                    {'args':['--clean-sessions'], 'default': False, 'action': 'store_true', 'help': 'clean all suspended sessions before starting a new one', },
                    {'args':['--terminate-on-ctrl-c'], 'default': False, 'action': 'store_true', 'help': 'terminate the connected session when pressing CTRL+C (instead of suspending the session)', },
                    {'args':['--auth-attempts'], 'default': 3, 'help': 'number of authentication attempts before authentication fails (default: 3)', },
                  ]
print_options =   [
                    {'args':['--print-action'], 'default': 'PDFVIEW', 'choices': PRINT_ACTIONS, 'help': 'action to be performed for incoming X2Go print jobs (default: \'PDFVIEW\')', },
                    {'args':['--pdfview-cmd'], 'default': None, 'help': 'PDF viewer command for displaying incoming X2Go print jobs (default: \'%s\'); this option selects \'--print-action PDFVIEW\'' % DEFAULT_PDFVIEW_CMD,},
                    {'args':['--save-to-folder'], 'default': None, 'metavar': 'PRINT_DEST', 'help': 'save print jobs as PDF files to folder PRINT_DEST (default: \'%s\'); this option selects \'--print-action PDFSAVE\'' % DEFAULT_PDFSAVE_LOCATION,},
                    {'args':['--printer'], 'default': None, 'help': 'target CUPS print queue for incoming X2Go print jobs (default: CUPS default printer); this option selects \'--print-action CUPS\'',},
                    {'args':['--print-cmd'], 'default': None, 'help': 'print command including cmd line arguments (default: \'%s\'); this option selects \'--print-action PRINTCMD\'' % DEFAULT_PRINTCMD_CMD,},
                  ]
broker_options =  [
                    {'args':['-B','--broker-url'], 'default': None, 'help': 'retrieve session profiles via an X2Go Session Broker under the given URL', },
                    {'args':['--broker-password'], 'default': None, 'help': 'password for authenticating against the X2Go Session Broker', },
                  ]

nx_options =      [
                    {'args':['-g','--geometry'], 'default': '800x600','help': 'screen geometry: \'<width>x<height>\' or \'fullscreen\' (default: \'800x600\')',},
                    {'args':['-q','--link'], 'default': 'adsl', 'choices': ('modem','isdn','adsl','wan','lan'), 'help': 'link quality (default: \'adsl\')',},
                    {'args':['-t','--session-type'], 'default': 'application', 'choices': ('desktop', 'application'), 'help': 'session type (default: \'application\')', },
                    {'args':['--pack'], 'default': '16m-jpeg-9', 'help': 'compression methods (see below for possible values)', },
                    {'args':['--kbd-layout'], 'default': 'us', 'help': 'use keyboard layout (default: \'us\')',},
                    {'args':['--kbd-type'], 'default': 'pc105/us', 'help': 'set Keyboard type (default: \'pc105/us\')',},
                  ]
compat_options =  [
                    {'args':['--port'], 'default': None, 'help': 'compatibility option, synonymous to --remote-ssh-port PORT', },
                    {'args':['--ssh-key'], 'default': None, 'help': 'compatibility option, synonymous to --ssh-privkey SSH_KEY', },
                    {'args':['--use-sound'], 'default': None, 'choices': ('yes', 'no'), 'help': 'compatibility option, synonymous to --sound {pulse|none}', },
                    {'args':['--client-ssh-port'], 'default': None, 'help': 'compatibility option for the x2goclient GUI; as Python X2Go brings its own internal SFTP server, this option will be ignored', },
                  ]

_profiles_backend_default = BACKENDS['X2GoSessionProfiles']['default']
_settings_backend_default = BACKENDS['X2GoClientSettings']['default']
_printing_backend_default = BACKENDS['X2GoClientPrinting']['default']

if _X2GOCLIENT_OS == 'Windows':
    _config_backends = ('FILE', 'WINREG')
elif _X2GOCLIENT_OS == 'Linux':
    _config_backends = ('FILE', 'GCONF')
else:
    _config_backends = ('FILE')



backend_options = [
                    {'args':['--backend-controlsession'], 'default': None, 'metavar': '<CONTROLSESSION_BACKEND>', 'choices': BACKENDS['X2GoControlSession'].keys(), 'help': 'force usage of a certain CONTROLSESSION_BACKEND (do not use this unless you know exactly what you are doing)', },
                    {'args':['--backend-terminalsession'], 'default': None, 'metavar': '<TERMINALSESSION_BACKEND>', 'choices': BACKENDS['X2GoTerminalSession'].keys(), 'help': 'force usage of a certain TERMINALSESSION_BACKEND (do not use this unless you know exactly what you are doing)', },
                    {'args':['--backend-serversessioninfo'], 'default': None, 'metavar': '<SERVERSESSIONINFO_BACKEND>', 'choices': BACKENDS['X2GoServerSessionInfo'].keys(), 'help': 'force usage of a certain SERVERSESSIONINFO_BACKEND (do not use this unless you know exactly what you are doing)', },
                    {'args':['--backend-serversessionlist'], 'default': None, 'metavar': '<SERVERSESSIONLIST_BACKEND>', 'choices': BACKENDS['X2GoServerSessionList'].keys(), 'help': 'force usage of a certain SERVERSESSIONLIST_BACKEND (do not use this unless you know exactly what you are doing)', },
                    {'args':['--backend-proxy'], 'default': None, 'metavar': '<PROXY_BACKEND>', 'choices': BACKENDS['X2GoProxy'].keys(), 'help': 'force usage of a certain PROXY_BACKEND (do not use this unless you know exactly what you are doing)', },
                    {'args':['--backend-sessionprofiles'], 'default': None, 'metavar': '<SESSIONPROFILES_BACKEND>', 'choices': _config_backends, 'help': 'use given backend for accessing session profiles, available backends on your system: %s (default: %s)' % (', '.join(_config_backends), _profiles_backend_default), },
                    {'args':['--backend-clientsettings'], 'default': None, 'metavar': '<CLIENTSETTINGS_BACKEND>', 'choices': _config_backends, 'help': 'use given backend for accessing the client settings configuration, available backends on your system: %s (default: %s)' % (', '.join(_config_backends), _settings_backend_default), },
                    {'args':['--backend-clientprinting'], 'default': None, 'metavar': '<CLIENTPRINTING_BACKEND>', 'choices': _config_backends, 'help': 'use given backend for accessing the client printing configuration, available backends on your system: %s (default: %s)' % (', '.join(_config_backends), _printing_backend_default), },
                  ]

###
### beginning of code
###

# print version text and exit
def version():

    sys.stderr.write ("%s\n" % VERSION_TEXT)
    sys.exit(0)


def parseargs():

    global logger
    global liblogger

    p = argparse.ArgumentParser(description='X2Go command line client implemented in Python.',\
                                epilog="""
Possible values for the --pack NX option are:
    %s
""" % x2go.defaults.pack_methods_nx3_formatted, \
                                formatter_class=argparse.RawDescriptionHelpFormatter, \
                                add_help=True, argument_default=None)
    p_reqargs = p.add_argument_group('X2Go server name is always required')
    p_reqargs.add_argument('--server', help='server hostname or IP address')
    p_actionopts = p.add_argument_group('client actions')
    p_debugopts = p.add_argument_group('debug options')
    p_x2goopts = p.add_argument_group('X2Go options')
    p_printopts = p.add_argument_group('X2Go print options')
    p_brokeropts = p.add_argument_group('X2Go Session Broker client options')
    p_nxopts = p.add_argument_group('NX options')
    p_backendopts = p.add_argument_group('Python X2Go backend options (for experts only)')
    p_compatopts = p.add_argument_group('compatibility options')

    for (p_group, opts) in ((p_x2goopts, x2go_options), (p_printopts, print_options), (p_brokeropts, broker_options), (p_actionopts, action_options), (p_debugopts, debug_options), (p_nxopts, nx_options), (p_backendopts, backend_options), (p_compatopts, compat_options)):
        for opt in opts:

            args = opt['args']
            del opt['args']
            p_group.add_argument(*args, **opt)

    a = p.parse_args()

    if a.debug:
        logger.set_loglevel_debug()

    if a.libdebug:
        liblogger.set_loglevel_debug()

    if a.quiet:
        logger.set_loglevel_quiet()
        liblogger.set_loglevel_quiet()

    if a.libdebug_sftpxfer:
        liblogger.enable_debug_sftpxfer()

    if a.password and _X2GOCLIENT_OS == "Windows":
        runtime_error("The --password option is forbidden on Windows platforms", parser=p, exitcode=222)

    if a.version:
        version()

    # if no username is given we use the uid under which this programme is run
    if a.username is None and not a.session_profile:
        a.username = current_user

    if not (a.session_profile or a.list_profiles):

        # the --server (or --session-profile) option is required for most operations
        if not a.server and not a.from_stdin:
            runtime_error ("argument --server (or --session-profile) is required", parser=p, exitcode=1)

        # check for mutual exclusiveness of -N, -R, -S, -T and -L, -N is default if none of them is set
        if bool(a.new) + bool(a.resume) + bool(a.share_desktop) + bool(a.suspend) + bool(a.terminate) + bool(a.list_sessions) + bool(a.list_desktops) + bool(a.list_profiles)> 1:
            runtime_error ("modes --new, --resume, --share-desktop, --suspend, --terminate, --list-sessions, --list-desktops and --list-profiles are mutually exclusive", parser=p, exitcode=2)
        if bool(a.new) + bool(a.resume) + bool(a.share_desktop) + bool(a.suspend) + bool(a.terminate) + bool(a.list_sessions) + bool(a.list_desktops) +  bool(a.list_profiles) == 0:
            a.new = True

        # check if pack method is available
        if not x2go.utils.is_in_nx3packmethods(a.pack):
            runtime_error("unknown pack method '%s'" % args.pack, parser=p, exitcode=10)

    else:
        if not (a.resume or a.share_desktop or a.suspend or a.terminate or a.list_sessions or a.list_desktops or a.list_profiles):
            a.new = True

    # X2Go printing
    if ((a.pdfview_cmd and a.printer) or
        (a.pdfview_cmd and a.save_to_folder) or
        (a.pdfview_cmd and a.print_cmd) or
        (a.printer    and a.save_to_folder) or
        (a.printer    and a.print_cmd) or
        (a.print_cmd  and a.save_to_folder)):
        runtime_error("--pdfviewer, --save-to-folder, --printer and --print-cmd options are mutually exclusive", parser=p, exitcode=81)

    if a.pdfview_cmd:
        a.print_action = 'PDFVIEW'
    elif a.save_to_folder:
        a.print_action = 'PDFSAVE'
    elif a.printer:
        a.print_action = 'PRINT'
    elif a.print_cmd:
        a.print_action = 'PRINTCMD'

    if a.pdfview_cmd is None and a.print_action == 'PDFVIEW':
        a.pdfview_cmd = DEFAULT_PDFVIEW_CMD

    if a.save_to_folder is None and a.print_action == 'PDFSAVE':
        a.save_to_folder = DEFAULT_PDFSAVE_LOCATION

    if a.printer is None and a.print_action == 'PRINT':
        # None means CUPS default printer...
        a.printer = None

    if a.print_cmd is None and a.print_action == 'PRINTCMD':
        a.print_cmd = DEFAULT_PRINTCMD_CMD

    a.print_action_args = {}
    if a.pdfview_cmd:
        a.print_action_args={'pdfview_cmd': a.pdfview_cmd, }
    elif a.save_to_folder:
        a.print_action_args={'save_to_folder': a.save_to_folder, }
    elif a.printer:
        a.print_action_args={'printer': a.printer, }
    elif a.print_cmd:
        a.print_action_args={'print_cmd': a.print_cmd, }

    ### take care of compatibility options
    # option --use-sound yes as synonomyn for --sound
    if a.use_sound is not None:
        if a.use_sound == 'yes': a.sound = 'pulse'
        if a.use_sound == 'no':  a.sound = 'none'

    if a.ssh_key is not None:
        a.ssh_privkey = a.ssh_key

    if a.port is not None:
        a.remote_ssh_port = a.port

    if a.share_local_folders is not None:
        a.share_local_folders = a.share_local_folders.split(',')

    try:
        _dummy = int(a.auth_attempts)
    except ValueError:
        runtime_error ("value for cmd line argument --auth-attempts has to be of type integer", parser=p, exitcode=1)

    if a.server:

        ##### TODO: ssh_config to be moved into Python X2Go!!!!
        ###
        ### initialize SSH context
        ###
        # check if SERVER is in .ssh/config file, extract information from there...
        ssh_config = paramiko.SSHConfig()
        from pyhoca.cli import ssh_config_filename
        ssh_config_fileobj = open(ssh_config_filename)
        ssh_config.parse(ssh_config_fileobj)
        ssh_host = ssh_config.lookup(a.server)
        if ssh_host:
            if 'hostname' in ssh_host.keys():
                a.server = ssh_host['hostname']
            if 'port' in ssh_host.keys():
                a.remote_ssh_port = ssh_host['port']
        ssh_config_fileobj.close()

    # check if ssh priv key exists
    if a.ssh_privkey and not os.path.isfile(a.ssh_privkey):
        runtime_error("SSH private key %s file does not exist." % a.ssh_privkey, parser=p, exitcode=30)
    if not a.ssh_privkey and os.path.isfile('%s/.ssh/id_rsa' % current_home):
        a.ssh_privkey = '%s/.ssh/id_rsa' % current_home
    if not a.ssh_privkey and os.path.isfile('%s/.ssh/id_dsa' % current_home):
        a.ssh_privkey = '%s/.ssh/id_dsa' % current_home

    # lightdm remote login magic takes place here
    if a.from_stdin:

        lightdm_remote_login_buffer = sys.stdin.readline()
        (a.username, a.server, a.command) = lightdm_remote_login_buffer.split()[0:3]
        a.password = " ".join(lightdm_remote_login_buffer.split()[3:])
        if ":" in a.server:
            a.remote_ssh_port = a.server.split(':')[-1]
            a.server = ':'.join(a.server.split(':')[:-1])
        a.command = a.command.upper()
        a.geometry = 'fullscreen'

    return p, a


if __name__ == '__main__':

    # parse command line
    parser, args = parseargs()
    args.parser = parser

    try:

        # initialize the X2GoClient context and start the connection to the X2Go server
        logger('preparing requested X2Go session', x2go.loglevel_NOTICE, )

        thisPyHocaCLI = PyHocaCLI(args, logger=logger, liblogger=liblogger)
        thisPyHocaCLI.authenticate()
        thisPyHocaCLI.MainLoop()

        sys.exit(0)

    except (KeyboardInterrupt, SystemExit), e:
        x2go.x2go_cleanup(e)

