#!/usr/bin/env python
# Copyright (C) 2010 Eduardo Robles Elvira <edulix AT gmail DOT com>
#
# This program 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.
#
# 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 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, see <http://www.gnu.org/licenses/>.

from __future__ import unicode_literals
import tempfile
import os
import json
import web
import re
from web.wsgiserver import CherryPyWSGIServer
from secure_connection import SecureConnection
from web.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
import config
from threading import Thread, Event
from subprocess import Popen, PIPE
from shlex import split
from sys import exit
import datetime
from urllib import urlencode
from socket import error as SocketError
from ssl import SSLError

from sys import path
path += ('../ogr',)
import libogr


# measured in seconds
MAX_INTERVAL_TIMEOUT = 3
MIN_INTERVAL_TIMEOUT = 1
OPENGNSYS_START_JOB_ID = 0
OPENGNSYS_STOP_JOB_ID = 1

SEQJOBS = []


class RepeatTimer(Thread):
    '''
    This is a timer that calls periodically a given function.

    Usage: Call the constructor specifiying at least the interval and the function,
    and then when you're ready, call to start(). It will launch a new thread that
    will periodically call to the given function.
    '''
    def __init__(self, interval, function, args=[], kwargs={}):
        Thread.__init__(self)
        self.interval = interval
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.finished = Event()
        self.restart = Event()

    def run(self):
        '''
        This is the real thing. Do NOT call directly to this function because
        you want it to be executed in another thread, call to start()
        '''
        while not self.finished.is_set():
            self.finished.wait(self.interval)
            while self.restart.is_set():
                self.finished.clear()
                self.restart.clear()
                self.finished.wait(self.interval)
            if not self.finished.is_set():
                self.function(*self.args, **self.kwargs)

    def cancel(self):
        '''
        Stops the timer. It will be of NO use anymore; if you want to restart it again
        you'll need to create a new timer. If you want to stop and then continue again
        later, call to pause_execution() instead.
        '''
        self.finished.set()

    def restart(self, interval=None):
        '''
        Stop and start the thread. This is usefull if for example you set interval to
        10 senconds, and 9 seconds passed, but suddently you want the timer counter to
        start counting from 0 to 10 again because another event came in.
        '''
        if interval:
            self.interval = interval
        self.finished.set()
        self.restart.set()

    def pause_execution():
        '''
        Pauses execution.
        '''
        self.temp_interval = self.interval
        self.interval = None
        self.finished.set()
        self.restart.set()

    def continue_execution(interval = None):
        '''
        Continues execution of the RepeatTimer after having called to pause_execution().
        It will start counting from zero.

        Do NOT call to this function without having called previously to pause_execution()
        or it will fail.
        '''
        if interval:
            self.interval = interval
        else:
            self.interval = self.temp_interval
        self.finished.set()

def launch_server():
    '''
    Launches a test server which always prints the args in the url requested
    '''
    CherryPyWSGIServer.ssl_adapter = pyOpenSSLAdapter(config.cert_file,
        config.key_file, None, config.ca_cert)

    urls = ("/jobrequest/(?P<id>[0-9]+)", "JobView")
    # the following is the only way to tell web.py which port to start the server..
    app = web.application(urls, globals()).wsgifunc()
    server = CherryPyWSGIServer((config.client_daemon_ip, config.client_daemon_port), app, server_name=config.client_daemon_name)
    print "launching server at %s:%d" % (config.client_daemon_ip, config.client_daemon_port)

    # Sends initial job that tells to the server that we're in opengnsys
    request = dict(command='GetIpAddress.sh', type='ogr')
    job = Job(OPENGNSYS_START_JOB_ID, json.dumps(request))

    try:
        server.start()
    except:
        server.stop()
    finally:
        job = Job(OPENGNSYS_STOP_JOB_ID, json.dumps(request))

class Job(object):
    def __init__(self, id, request):
        '''
        id: job id
        request: = {
            'command': command,
            'type': "ogr|command",
            ['args': args,]
            ['files': files,]
            ['sequential': True | False]
        }
        '''

        self.id = id
        self.request = json.loads(request)

        # setting default values
        if not self.request.has_key('args'):
            self.request['args'] = []
        if not self.request.has_key('files'):
            self.request['files'] = []
        if not self.request.has_key('sequential'):
            self.request['sequential'] = False

        self.tmpfiles = []
        self.last_status = "SENT"
        self.last_message = ""
        self.timer = RepeatTimer(MAX_INTERVAL_TIMEOUT, self.update_timeout)
        self.timer.start()
        self.last_update_time = datetime.datetime.now()

        if not self.request['sequential']:
            self.launch()
        else:
            SEQJOBS.append(self)
            if SEQJOBS[0] == self:
                self.launch_sequential()

    def launch(self):
        self.thread = Thread(target=self.execute_command)
        self.thread.start()

    def launch_sequential(self):
        if SEQJOBS[0] == self:
            self.launch()

    def finish(self):
        self.remove_tmp_files()
        if self.request['sequential']:
            SEQJOBS.pop(0)
            if len(SEQJOBS) > 0:
                SEQJOBS[0].launch_sequential()

    def get_command(self):
        # creating temp files
        fnames = []
        for f in self.request['files']:
            fname = tempfile.mktemp()
            fnames.append(fname)
            with open(fname, "w") as tfile:
                tfile.write(f)

        # replacing file args
        args = []
        for arg in self.request['args']:
            narg = arg
            if arg.startswith('$'):
                try:
                    narg = fnames[int(arg[1:])]
                except ValueError, IndexError:
                    narg = arg
            args.append(narg)

        command = [self.request['command']] + args
        self.tmpfiles = fnames
        return command

    def remove_tmp_files(self):
        for f in self.tmpfiles:
            os.unlink(f)

    def execute_command(self):
        '''
        Executes the requested command. This is executed in a thread, as it is
        a blocking operation. Meanwhile, the repeat timer keeps the server
        updated, in case the command takes too much to execute.
        '''
        command = self.get_command()
        if self.request['type'] == 'ogr':
            self.execute_ogr_command(command)
        else: # native command
            self.execute_native_command(command)
        self.finish()

    def execute_ogr_command(self, command):
        try:
            proc = libogr.Command(command)
            proc.run(progress_cb=self.send_progress_update)
            if proc.return_value:
                raise Exception("ERROR", "Return code: %d" % proc.return_value)
            self.timer.cancel()
            self.send_update("FINISHED", proc.output)
        except Exception, e:
            self.timer.cancel()
            print e.__str__()
            self.send_update("ERROR", e.__str__())

    def execute_native_command(self, command):
        try:
            proc = Popen(command, stdout=PIPE, stderr=PIPE)
            output, unused_stderr = proc.communicate()

            if proc.returncode:
                raise Exception("Return code: %d" % proc.returncode)
            self.timer.cancel()
            self.send_update("FINISHED", output)
        except Exception, e:
            self.timer.cancel()
            print e.__str__()
            self.send_update("ERROR", e.__str__())

    def update_timeout(self):
        '''
        Function called every MAX_INTERVAL_TIMEOUT to ensure the server receives an
        INPROGRESS call at least every MAX_INTERVAL_TIMEOUT.
        '''
        current_time = datetime.datetime.now()
        if current_time - self.last_update_time < datetime.timedelta(seconds = MIN_INTERVAL_TIMEOUT):
            return
        self.send_update("INPROGRESS", self.last_message)

    def send_progress_update(self, progress, message):
        '''
        Sends a progress update to the server daemon
        '''
        print  "send_progress_update", progress, message
        connection = SecureConnection(config.server_daemon_ip, config.server_daemon_port)
        try:
            url = '/updatejob/progress/%s/%s' % (self.id, progress)
            post_data = urlencode({'message': message})
            headers = {"Content-type": "application/x-www-form-urlencoded",
                "Accept": "text/plain"}
            connection.request('POST', url, post_data, headers)
            response = connection.getresponse()
            response_data = response.read()
            print config.server_daemon_ip, config.server_daemon_port, url, "response.status", response.status
        except SocketError, e:
            print "SocketError", e.__str__()
        except SSLError, e:
            print "SSLError", e.__str__()
        connection.close()

    def send_update(self, status, message):
        '''
        Sends an update to the server daemon about a job being done
        '''
        print  "send_update", status, message
        connection = SecureConnection(config.server_daemon_ip, config.server_daemon_port)
        try:
            url = '/updatejob/normal/%s/%s' % (self.id, status)
            post_data = urlencode({'message': message})
            headers = {"Content-type": "application/x-www-form-urlencoded",
                "Accept": "text/plain"}
            connection.request('POST', url, post_data, headers)
            response = connection.getresponse()
            response_data = response.read()
            print config.server_daemon_ip, config.server_daemon_port, url, "response.status", response.status
        except SocketError, e:
            print "SocketError", e.__str__()
        except SSLError, e:
            print "SSLError", e.__str__()
        connection.close()

class JobView:
    def launch_job(self):
        '''
        Launches a job, called in a new thread that keeps running in background in the server
        '''
        Job(self.id, self.request)

    def POST(self, id):
        self.request = web.input().request
        self.id = id
        print self, id, self.request
        thread = Thread(target=self.launch_job)
        thread.start()
        return

if __name__ == "__main__":
    '''
    Launches the client daemon which receives updates from the clients
    '''
    launch_server()
