'''
Buildozer remote
================

.. warning::

    This is an experimental tool and not widely used. It might not fit for you.

Pack and send the source code to a remote SSH server, bundle buildozer with it,
and start the build on the remote.
You need paramiko to make it work.
'''

__all__ = ["BuildozerRemote"]

import socket
import sys
from buildozer import (
    Buildozer, BuildozerCommandException, BuildozerException, __version__)
from sys import stdout, stdin, exit
from select import select
from os.path import join, expanduser, realpath, exists, splitext
from os import makedirs, walk, getcwd
try:
    from configparser import SafeConfigParser
except ImportError:
    from ConfigParser import SafeConfigParser
try:
    import termios
    import tty
    has_termios = True
except ImportError:
    has_termios = False
try:
    import paramiko
except ImportError:
    print('Paramiko missing: pip install paramiko')


class BuildozerRemote(Buildozer):
    def run_command(self, args):
        while args:
            if not args[0].startswith('-'):
                break
            arg = args.pop(0)

            if arg in ('-v', '--verbose'):
                self.log_level = 2

            elif arg in ('-p', '--profile'):
                self.config_profile = args.pop(0)

            elif arg in ('-h', '--help'):
                self.usage()
                exit(0)

            elif arg == '--version':
                print('Buildozer (remote) {0}'.format(__version__))
                exit(0)

        self._merge_config_profile()

        if len(args) < 2:
            self.usage()
            return

        remote_name = args[0]
        remote_section = 'remote:{}'.format(remote_name)
        if not self.config.has_section(remote_section):
            self.error('Unknown remote "{}", must be configured first.'.format(
                remote_name))
            return

        self.remote_host = remote_host = self.config.get(
                remote_section, 'host', '')
        self.remote_port = self.config.get(
                remote_section, 'port', '22')
        self.remote_user = remote_user = self.config.get(
                remote_section, 'user', '')
        self.remote_build_dir = remote_build_dir = self.config.get(
                remote_section, 'build_directory', '')
        self.remote_identity = self.config.get(
                remote_section, 'identity', '')
        if not remote_host:
            self.error('Missing "host = " for {}'.format(remote_section))
            return
        if not remote_user:
            self.error('Missing "user = " for {}'.format(remote_section))
            return
        if not remote_build_dir:
            self.error('Missing "build_directory = " for {}'.format(remote_section))
            return

        # fake the target
        self.targetname = 'remote'
        self.check_build_layout()

        # prepare our source code
        self.info('Prepare source code to sync')
        self._copy_application_sources()
        self._ssh_connect()
        try:
            self._ensure_buildozer()
            self._sync_application_sources()
            self._do_remote_commands(args[1:])
            self._ssh_sync(getcwd(), mode='get')
        finally:
            self._ssh_close()

    def _ssh_connect(self):
        self.info('Connecting to {}'.format(self.remote_host))
        self._ssh_client = client = paramiko.SSHClient()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        client.load_system_host_keys()
        kwargs = {}
        if self.remote_identity:
            kwargs['key_filename'] = expanduser(self.remote_identity)
        client.connect(self.remote_host, username=self.remote_user,
                port=int(self.remote_port), **kwargs)
        self._sftp_client = client.open_sftp()

    def _ssh_close(self):
        self.debug('Closing remote connection')
        self._sftp_client.close()
        self._ssh_client.close()

    def _ensure_buildozer(self):
        s = self._sftp_client
        root_dir = s.normalize('.')
        self.remote_build_dir = join(root_dir, self.remote_build_dir,
                self.package_full_name)
        self.debug('Remote build directory: {}'.format(self.remote_build_dir))
        self._ssh_mkdir(self.remote_build_dir)
        self._ssh_sync(__path__[0])

    def _sync_application_sources(self):
        self.info('Synchronize application sources')
        self._ssh_sync(self.app_dir)

        # create custom buildozer.spec
        self.info('Create custom buildozer.spec')
        config = SafeConfigParser()
        config.read('buildozer.spec')
        config.set('app', 'source.dir', 'app')

        fn = join(self.remote_build_dir, 'buildozer.spec')
        fd = self._sftp_client.open(fn, 'wb')
        config.write(fd)
        fd.close()

    def _do_remote_commands(self, args):
        self.info('Execute remote buildozer')
        cmd = (
            'source ~/.profile;'
            'cd {0};'
            'env PYTHONPATH={0}:$PYTHONPATH '
            'python -c "import buildozer, sys;'
            'buildozer.Buildozer().run_command(sys.argv[1:])" {1} {2} 2>&1').format(
            self.remote_build_dir,
            '--verbose' if self.log_level == 2 else '',
            ' '.join(args),
            )
        self._ssh_command(cmd)

    def _ssh_mkdir(self, *args):
        directory = join(*args)
        self.debug('Create remote directory {}'.format(directory))
        try:
            self._sftp_client.mkdir(directory)
        except IOError:
            # already created?
            try:
                self._sftp_client.stat(directory)
            except IOError:
                self.error('Unable to create remote directory {}'.format(directory))
                raise

    def _ssh_sync(self, directory, mode='put'):
        self.debug('Syncing {} directory'.format(directory))
        directory = realpath(directory)
        base_strip = directory.rfind('/')
        if mode == 'get':
            local_dir = join(directory,'bin')
            remote_dir = join(self.remote_build_dir, 'bin')
            if not exists(local_dir):
                makedirs(local_dir)
            for _file in self._sftp_client.listdir(path=remote_dir):
                self._sftp_client.get(join(remote_dir, _file),
                                      join(local_dir, _file))
            return
        for root, dirs, files in walk(directory):
            self._ssh_mkdir(self.remote_build_dir, root[base_strip + 1:])
            for fn in files:
                if splitext(fn)[1] in ('.pyo', '.pyc', '.swp'):
                    continue
                local_file = join(root, fn)
                remote_file = join(self.remote_build_dir, root[base_strip + 1:], fn)
                self.debug('Sync {} -> {}'.format(local_file, remote_file))
                self._sftp_client.put(local_file, remote_file)

    def _ssh_command(self, command):
        self.debug('Execute remote command {}'.format(command))
        #shell = self._ssh_client.invoke_shell()
        #shell.sendall(command)
        #shell.sendall('\nexit\n')
        transport = self._ssh_client.get_transport()
        channel = transport.open_session()
        try:
            channel.exec_command(command)
            self._interactive_shell(channel)
        finally:
            channel.close()

    def _interactive_shell(self, chan):
        if has_termios:
            self._posix_shell(chan)
        else:
            self._windows_shell(chan)

    def _posix_shell(self, chan):
        oldtty = termios.tcgetattr(stdin)
        try:
            #tty.setraw(stdin.fileno())
            #tty.setcbreak(stdin.fileno())
            chan.settimeout(0.0)

            while True:
                r, w, e = select([chan, stdin], [], [])
                if chan in r:
                    try:
                        x = chan.recv(128)
                        if len(x) == 0:
                            print('\r\n*** EOF\r\n',)
                            break
                        stdout.write(x)
                        stdout.flush()
                        #print len(x), repr(x)
                    except socket.timeout:
                        pass
                if stdin in r:
                    x = stdin.read(1)
                    if len(x) == 0:
                        break
                    chan.sendall(x)
        finally:
            termios.tcsetattr(stdin, termios.TCSADRAIN, oldtty)

    # thanks to Mike Looijmans for this code
    def _windows_shell(self,chan):
        import threading

        stdout.write("Line-buffered terminal emulation. Press F6 or ^Z to send EOF.\r\n\r\n")

        def writeall(sock):
            while True:
                data = sock.recv(256)
                if not data:
                    stdout.write('\r\n*** EOF ***\r\n\r\n')
                    stdout.flush()
                    break
                stdout.write(data)
                stdout.flush()

        writer = threading.Thread(target=writeall, args=(chan,))
        writer.start()

        try:
            while True:
                d = stdin.read(1)
                if not d:
                    break
                chan.send(d)
        except EOFError:
            # user hit ^Z or F6
            pass

def main():
    try:
        BuildozerRemote().run_command(sys.argv[1:])
    except BuildozerCommandException:
        pass
    except BuildozerException as error:
        Buildozer().error('%s' % error)

if __name__ == '__main__':
    main()
