#!/usr/bin/env python
from __future__ import print_function, division, unicode_literals
from tornado.escape import json_encode, json_decode
import tornado.ioloop
from tornado.web import RequestHandler, Application, url, HTTPError
import yaml
import subprocess
import traceback
import time

from hcam_drivers.utils.obsmodes import get_obsmode, Idle


MSG_TEMPLATE = "MESSAGEBUFFER: {}\nRETCODE: {}"

# This script provides a "thin client" that runs on the rack PC.
# The thin client acts as a bridge between client software on
# another machine and the ESO software, controlling the camera and
# receiving info about the current status


def sendCommand(command, *command_pars):
    """
    Use low level ngcbCmd to send command to control server.
    """
    command_array = ['ngcbCmd', command]
    if command_pars:
        command_array.extend(*command_pars)
    # must contain only strings
    command_array = [str(val) for val in command_array]

    # output for debugging
    frame_command = len(command_array) == 3 and command_array[2] == 'DET.FRAM2.NO'
    if not frame_command:
        print(command_array)

    # now attempt to send command to control server
    try:
        ret_msg = subprocess.check_output(command_array).strip()
        # make sure all successful commands end with OK line
        result = MSG_TEMPLATE.format(ret_msg, "OK")
    except subprocess.CalledProcessError as err:
        # make sure all failed commands end with NOK line
        result = MSG_TEMPLATE.format(err.output.strip(), "NOK")
    return result


def databaseSummary():
    """
    Get a summary of system state and exposure state from database
    """
    cmdTemplate = 'dbRead "<alias>ngcircon_ircam1:"{}'
    database_attributes = [
        'system.stateName',
        'system.subStateName',
        'exposure.time',  # total exposure time for run
        'exposure.countDown',  # time remaining
        'exposure.expStatusName',
        'exposure.baseName',
        'exposure.newDataFileName']  # LAST WRITTEN FILE
    results = {}
    for attribute in database_attributes:
        cmd = cmdTemplate.format(attribute)
        try:
            response = subprocess.check_output(cmd, shell=True)
            results[attribute] = response.split('=')[-1].strip()
        except:
            results[attribute] = 'ERR'
    results['RETCODE'] = "OK"
    return results


def parse_response(response):
    """
    Take server response and convert to well formed JSON.
    """
    return json_encode(yaml.load(response))


class HServerException(HTTPError):
    pass


class BaseHandler(RequestHandler):
    """
    Abstract class for request handling
    """
    def initialize(self, db):
        self.db = db
        self.command = None

    def write_error(self, status_code, **kwargs):
        self.set_header('Content-Type', 'application/json')
        resp = MSG_TEMPLATE.format(self._reason, 'NOK')
        resp_dict = yaml.load(resp)
        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            lines = []
            for line in traceback.format_exception(*kwargs["exc_info"]):
                lines.append(line)
            resp_dict['traceback'] = lines
        self.finish(json_encode(resp_dict))

    def get(self):
        """
        Execute server command, return response
        """
        response = sendCommand(self.command)
        self.set_header('Content-Type', 'application/json')
        self.write(parse_response(response))


class StartHandler(BaseHandler):
    """
    Start a run
    """
    def initialize(self, db):
        self.db = db
        self.command = 'start'


class StopHandler(BaseHandler):
    """
    Stop a run, returning intermediate product
    """
    def initialize(self, db):
        self.db = db
        self.command = 'end'


class AbortHandler(BaseHandler):
    """
    Abort a run
    """
    def initialize(self, db):
        self.db = db
        self.command = 'abort'


class OnlineHandler(BaseHandler):
    """
    Bring server online, powering on NGC controller
    """
    def initialize(self, db):
        self.db = db
        self.command = 'online'


class PowerOnHandler(BaseHandler):
    """
    Power on clock voltages
    """
    def initialize(self, db):
        self.db = db
        self.command = 'cldc 0 enable'


class PowerOffHandler(BaseHandler):
    """
    Power on clock voltages
    """
    def initialize(self, db):
        self.db = db
        self.command = 'cldc 0 disable'


class SeqStartHandler(BaseHandler):
    """
    Start sequencer running
    """
    def initialize(self, db):
        self.db = db
        self.command = 'seq 0 start'


class SeqStopHandler(BaseHandler):
    """
    Stop sequencer running
    """
    def initialize(self, db):
        self.db = db
        self.command = 'seq 0 stop'


class OfflineHandler(BaseHandler):
    """
    Bring server to OFF state. All sub-processes terminate. Server cannot reply
    """
    def initialize(self, db):
        self.db = db
        self.command = 'off'


class StandbyHandler(BaseHandler):
    """
    Bring server to standby state.

    All sub-processes are disabled, but server can communicate.
    """
    def initialize(self, db):
        self.db = db
        self.command = 'standby'


class ResetHandler(BaseHandler):
    """
    Resets the NGC controller front end
    """
    def initialize(self, db):
        self.db = db
        self.command = 'reset'


class SummaryHandler(BaseHandler):
    """
    Get status summary of server and exposure from the database
    """
    def get(self):
        summary_dictionary = databaseSummary()
        self.set_header('Content-Type', 'application/json')
        self.write(json_encode(summary_dictionary))


class StatusHandler(BaseHandler):
    """
    Check status, either of the server as a whole, or get/set
    parameter of the current sequencer file.
    """
    def get(self, param_id=None):
        # get server status
        if param_id is None:
            response = sendCommand('status')
            self.set_header('Content-Type', 'application/json')
            self.finish(parse_response(response))
        else:
            response = sendCommand('status', [param_id])
            self.set_header('Content-Type', 'application/json')
            self.finish(parse_response(response))

    def post(self, param_id):
        try:
            req_json = json_decode(self.request.body.decode())
            if not req_json or 'value' not in req_json:
                raise HServerException(reason='No value supplied',
                                       status_code=400)

            response = sendCommand('setup', [param_id, req_json['value']])
            self.set_header('Content-Type', 'application/json')
            self.finish(parse_response(response))
        except:
            raise HServerException(reason='Error setting param', status_code=500)


class PostRunHandler(BaseHandler):
    """
    Update multiple settings.

    Settings are JSON encoded in body of request.

    First we extract the application and load the correct sequencer file.
    Then we use 'ngcbCmd setup <key1> <value1> [<key2> <value2>] ....' to
    load remaining parameters.

    The remaining parameters are read from the dictionary created
    from the JSON data.
    """
    def post(self):
        req_json = json_decode(self.request.body.decode())
        if not req_json or 'appdata' not in req_json:
                raise HServerException(reason='No appdata supplied',
                                       status_code=400)

        try:
            obsmode = get_obsmode(req_json)
        except ValueError:
            raise HServerException(reason='Error parsing readout mode', status_code=500)

        time.sleep(0.1)
        self.set_header('Content-Type', 'application/json')
        response = yaml.load(sendCommand('seq stop'))
        if not response['RETCODE'] == "OK":
            raise HServerException(reason='could not stop sequencer', status_code=500)
        time.sleep(0.1)

        response = yaml.load(sendCommand(obsmode.readmode_command))
        if not response['RETCODE'] == "OK":
            self.write(json_encode(response))
            return
        time.sleep(0.1)

        response = sendCommand(obsmode.setup_command)
        retMsg = parse_response(response)
        time.sleep(0.1)

        if isinstance(obsmode, Idle):
            # a run start will start the sequencer automatically, and save data,
            # but there is no run start when we switch into idle mode, so start the sequencer by hand
            response = yaml.load(sendCommand('seq start'))
            if not response['RETCODE'] == "OK":
                raise HServerException(reason='could not start sequencer', status_code=500)

        self.finish(retMsg)


if __name__ == '__main__':
    db = {}
    app = Application([
        url(r'/start', StartHandler, dict(db=db), name="start"),
        url(r'/stop', StopHandler, dict(db=db), name="stop"),
        url(r'/abort', AbortHandler, dict(db=db), name="abort"),
        url(r'/online', OnlineHandler, dict(db=db), name="online"),
        url(r'/offline', OfflineHandler, dict(db=db), name="offline"),
        url(r'/pon', PowerOnHandler, dict(db=db), name='on'),
        url(r'/poff', PowerOffHandler, dict(db=db), name='off'),
        url(r'/seqStart', SeqStartHandler, dict(db=db), name='seqStart'),
        url(r'/seqStop', SeqStopHandler, dict(db=db), name='seqStart'),
        url(r'/reset', ResetHandler, dict(db=db), name="reset"),
        url(r'/standby', StandbyHandler, dict(db=db), name="standby"),
        url(r'/summary', SummaryHandler, dict(db=db), name="summary"),
        url(r'/status', StatusHandler, dict(db=db)),
        url(r'/setup', PostRunHandler, dict(db=db), name='setup'),
        url(r"/status/(.*)", StatusHandler, dict(db=db))
    ])
    app.listen(5000)
    tornado.ioloop.IOLoop.current().start()
