#!/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
import os
from io import BytesIO

from astropy.io import ascii
from astropy.io import fits
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 TriggerHandler(BaseHandler):
    """
    Send a trigger to the sequencer, which will start exposure
    """
    def initialize(self, db):
        self.db = db
        self.command = 'seq trigger'


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 = yaml.load(sendCommand(obsmode.acq_command))
        if not response['RETCODE'] == "OK":
            self.write(json_encode(response))
            return
        time.sleep(0.1)

        for header_command in obsmode.header_commands:
            response = yaml.load(sendCommand(header_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)


class InsertFITSTableHandler(BaseHandler):
    """
    Handle an uploaded table, and append it to the appropriate FITS file

    Filename is included in POST data, table is a binary encoded VOTable, which
    we will write to a FITS HDU, which is appended onto the original FITS file.
    """
    def post(self):
        try:
            fileinfo = self.request.files['file'][0]
            filename = os.path.join('/data', self.get_argument('run'))
            # put binary data into BytesIO object to read from
            table_data = BytesIO(fileinfo['body'])
        except:
            raise HServerException(reason='malformed POST request', status_code=500)

        try:
            t = ascii.read(table_data, format='ecsv')
        except:
            raise HServerException(reason='could not decode table data', status_code=500)

        try:
            new_hdu = fits.table_to_hdu(t)
            existing_hdu_list = fits.open(
                filename, mode='append', memmap=True
            )
            existing_hdu_list.append(new_hdu)
            existing_hdu_list.flush()
        except:
            raise HServerException(reason='could not write HDU to run', status_code=500)

        retmsg = json_encode({'MESSAGEBUFFER': 'DONE', 'RETCODE': 'OK'})
        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'/trigger', TriggerHandler, dict(db=db), name='trigger'),
        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'/addhdu', InsertFITSTableHandler, dict(db=db), name='addhdu'),
        url(r"/status/(.*)", StatusHandler, dict(db=db))
    ])
    app.listen(5000)
    tornado.ioloop.IOLoop.current().start()
