#!/usr/bin/env python
from __future__ import print_function, division, unicode_literals
import glob
import os
import sys
import time
import datetime
from itertools import cycle
import traceback
import subprocess

import tornado.ioloop
from tornado.web import RequestHandler, Application, url, HTTPError
from tornado.escape import json_encode, json_decode
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler

from hcam_widgets.gtc.corba import get_telescope_server


def send_trigger_when_ready(timeout):
    """
    Wait for telescope to be ready to observe, with timeout.

    Arguments
    ---------
    timeout : int
        time to wait before carrying on anyway
    """
    if timeout <= 0:
        print('WARNING: timed out waiting for telescope to be ready')
        print(' starting exposure anyway')
        subprocess.Popen('ngcbCmd seq trigger'.split())
        return

    print('Checking if telescope is ready')
    server = get_telescope_server()

    # is telescope on target?
    try:
        ready = server.areMACSFollowErrorBelowThreshold()
        # has mirror settled?
        ready = ready and server.isM1NoiseBelowThreshold()
    except:
        # failed to read status, don't assume OK.
        ready = False

    if ready:
        # ready to go, send trigger
        print('telescope ready - starting exposure')
        subprocess.Popen('ngcbCmd seq trigger'.split())
    else:
        # not ready yet, so call again in one second
        print('telescope not ready yet - check again in 1s')
        timeout -= 1
        tornado.ioloop.IOLoop.instance().add_timeout(
            datetime.timedelta(seconds=1), send_trigger_when_ready, timeout
        )


class FITSWriteHandler(PatternMatchingEventHandler):
    """
    Class to handle FITS write events.abs

    Upon write events to the current run, we send offset to
    the telescope, schedule callbacks to see if the
    offset is complete, and then send a trigger to the
    sequencer to continue with the observation.

    Arguments
    ----------
    path : str
        location of fits files to monitor
    ra_offsets : itertools.cycle
        absolute RA offsets in arcseconds
    dec_offsets : itertools.cycle
        absolute Dec offsets in arcseconds
    """
    patterns = ['*.fits']

    def __init__(self, path, ra_offsets, dec_offsets, *args, **kwargs):
        super(FITSWriteHandler, self).__init__()
        self.path = path
        self.ra_offsets = ra_offsets
        self.dec_offsets = dec_offsets
        try:
            pattern = os.path.join(self.path, self.patterns[0])
            self.existing_runs = glob.glob(pattern)
        except:
            self.existing_runs = []
        # used to avoid responding to very recent
        # events which may be duplicates.
        self.debounce_time = 0.1
        self.last_event = 0
        # offsets are supplied as absolute values, but telescope wants
        # relative offsets. Keep track of cumulative offsets to date
        # to allow conversion
        self.cumulative_ra_offset = 0.0
        self.cumulative_dec_offset = 0.0

    def get_relative_offsets(self, abs_ra_offset, abs_dec_offset):
        return (abs_ra_offset - self.cumulative_ra_offset,
                abs_dec_offset - self.cumulative_dec_offset)

    def do_offsets(self, raoff, decoff):
        server = get_telescope_server()
        try:
            server.requestTelescopeOffset(raoff, decoff)
        except Exception as err:
            print('offset failed!\n' + str(err))
            return False

        self.cumulative_ra_offset += raoff
        self.cumulative_dec_offset += decoff
        return True

    def check_debounce(self):
        """
        Avoid handling very closely occurring filesystem events.abs

        Sometimes there are two or more rapid filesystem events when
        a frame is written. This routine checks if another event
        as happened recently, and returns False if so.
        """
        first_event = False
        if time.time() - self.last_event > self.debounce_time:
            self.last_event = time.time()
            first_event = True
        else:
            self.last_event = time.time()
        return first_event

    def on_modified(self, event):
        if self.check_debounce():
            if event.src_path == self.existing_runs[-1]:
                # latest file has just been written to

                # get next relative offsets
                next_ra_offset, next_dec_offset = self.get_relative_offsets(
                    next(self.ra_offsets), next(self.dec_offsets)
                )
                # send telescope offset command
                print('sending tel offsets {} {}'.format(
                    next_ra_offset, next_dec_offset
                ))
                self.do_offsets(next_ra_offset, next_dec_offset)

                # launch thread to wait for telescope to be in place
                # and send trigger to NGC controller
                tornado.ioloop.IOLoop.instance().add_callback(
                    send_trigger_when_ready, 10
                )

    def on_created(self, event):
        self.check_debounce()
        # if we don't know about this, it's a new run.
        if event.src_path not in self.existing_runs:
            self.existing_runs.append(event.src_path)


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

    def write_error(self, status_code, **kwargs):
        self.set_header('Content-Type', 'application/json')
        resp_dict = dict(status='NOK')
        print(kwargs)
        if "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))


class StartHandler(BaseHandler):
    def get(self):
        print('starting')
        if 'ra_offs' not in self.db or 'dec_offs' not in self.db:
            raise HTTPError(reason='No offsets define. Post a setup first',
                            status_code=500)

        self.db['observer'] = make_observer(self.db['path'],
                                            self.db['ra_offs'],
                                            self.db['dec_offs'])
        db['observer'].start()
        self.finish({'status': 'OK', 'action': 'start'})


class StopHandler(BaseHandler):
    def get(self):
        print('stopping')
        if self.db.get('observer'):
            try:
                self.db['observer'].stop()
            except Exception as err:
                raise HTTPError(reason='Failed to stop offsetter.\n' + str(err),
                                status_code=500)

        self.finish({'status': 'OK', 'action': 'stop'})


class PostOffsetPatternHandler(BaseHandler):
    """
    Receive and store a POST request with ra and dec offsets

    Offsets are JSON encoded in body of request as lists.

    We create itertools.cycle objects and store them on the application
    database.
    """
    def post(self):
        req_json = json_decode(self.request.body.decode())
        if not req_json or 'appdata' not in req_json:
            raise HTTPError(reason='Malformed JSON in body of request',
                            status_code=400)
        req_json = req_json['appdata']

        if 'nodpattern' not in req_json:
            # nothing to do
            retMsg = json_encode({'status': 'OK', 'action': 'none'})
            self.finish(retMsg)
            return

        req_json = req_json['nodpattern']
        if 'ra' not in req_json or 'dec' not in req_json:
            raise HTTPError(reason='RA or Dec offsets missing from appdata',
                            status_code=400)

        try:
            if len(req_json['ra']) != len(req_json['dec']):
                raise ValueError('mismatched lengths')
        except (ValueError, TypeError):
            raise HTTPError(reason='Offsets either different lengths, or not iterable',
                            status_code=500)

        try:
            ra_offs = cycle(req_json['ra'])
            dec_offs = cycle(req_json['dec'])
        except ValueError:
            raise HTTPError(reason='Could not create offset patterns from JSON',
                            status_code=500)

        self.db['ra_offs'] = ra_offs
        self.db['dec_offs'] = dec_offs

        retMsg = json_encode({'status': 'OK', 'action': 'setup'})
        self.finish(retMsg)


def make_observer(path, ra_offs, dec_offs):
    observer = Observer()
    handler = FITSWriteHandler(path, ra_offs, dec_offs)
    observer.schedule(handler, path=path, recursive=False)
    observer.daemon = True
    return observer


if __name__ == "__main__":
    path = sys.argv[1]

    db = dict(path=path)
    app = Application([
        url(r'/start', StartHandler, dict(db=db), name='start'),
        url(r'/stop', StopHandler, dict(db=db), name='stop'),
        url(r'/setup', PostOffsetPatternHandler, dict(db=db), name='setup')
    ])
    app.listen(5001)
    tornado.ioloop.IOLoop.current().start()
