#!/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 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


def sendSeqTrigger():
    print('ngcbCmd seq trigger')


def scheduleTrigger(inTime):
    """
    Schedule a sendSeqTrigger request

    This can be called from any thread to schedule a trigger. It is needed
    because ``IOLoop.add_timeout`` is not thread safe and so can only be
    called from the IOLoop's own thread.

    Parameters
    ----------
    inTime : float
        time in seconds to wait before sending trigger
    """
    tornado.ioloop.IOLoop.instance().add_timeout(
        datetime.timedelta(seconds=inTime), sendSeqTrigger
    )


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.
    """
    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

    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

                # send telescope offset command
                print('sending tel offsets {} {}'.format(
                    next(self.ra_offsets), next(self.dec_offsets)
                ))

                # assume offset takes X seconds
                # trigger sequencer to start again in X s
                tornado.ioloop.IOLoop.instance().add_callback(
                    scheduleTrigger, 3
                )

    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()
