#!/usr/bin/env python
from __future__ import print_function, division, unicode_literals

import argparse
import os
import logging
import txaio
import six
from six.moves import queue

from autobahn.twisted.component import Component, run
from autobahn.twisted.wamp import ApplicationSession

from twisted.internet import reactor, tksupport
from twisted.internet.defer import inlineCallbacks

from hcam_widgets.globals import Container
import hcam_widgets.widgets as w
from hcam_widgets import hcam
from hcam_widgets.misc import execCommand, FifoThread, isPoweredOn, isOnline
from hcam_widgets.tkutils import addStyle
from hcam_widgets.hardware.slide import SlideFrame
from hcam_widgets.hardware.ccds import CCDTempFrame
from hcam_widgets.hardware import CCDInfoWidget
from hcam_widgets.compo.widgets import COMPOControlWidget

from hcam_drivers.utils.rtplot import RtplotFactory
from hcam_drivers.config import (load_config, write_config,
                                 check_user_dir, dump_app)

txaio.use_twisted()
if not six.PY3:
    import Tkinter as tk
    import tkFileDialog as filedialog
    import tkMessageBox as messagebox
else:
    import tkinter as tk
    from tkinter import filedialog, messagebox


usage = """
Python GUI for HiperCAM

Author: Stuart Littlefair
"""


class GUI(tk.Tk):
    """
    This class isolates the gui components from the rtplot server.
    """
    def __init__(self):
        # Create the main GUI
        tk.Tk.__init__(self)

        # add a container object
        self.globals = Container(telescope_names=['GTC', 'WHT'])

        # load config
        self.globals.cpars = dict()
        load_config(self.globals)
        # add one extra that there is no point getting from a file as
        # it should always be False on starting the GUI
        self.globals.cpars['eso_server_online'] = False

        if self.globals.cpars['debug']:
            logging.basicConfig(level=logging.DEBUG)
        else:
            logging.basicConfig(level=logging.INFO)

        # initialise WAMP session
        self.globals.session = None

        # style
        addStyle(self)

        # define frames for LHS and RHS, and bottom
        lhs = tk.Frame(self)
        rhs = tk.Frame(self)
        bottom = tk.Frame(self)

        # Now we make the various widgets. The order here is determined by the fact
        # that some widgets need to reference the simpler ones, so they have to
        # initialised first.

        # command log
        self.globals.clog = w.LabelGuiLogger('CMM', bottom, 5, 56, 'Command log')
        # server response log
        self.globals.rlog = w.LabelGuiLogger('RSP', bottom, 5, 56, 'Response log')

        # Instrument params
        self.globals.ipars = hcam.InstPars(rhs)

        # Run parameters
        self.globals.rpars = hcam.RunPars(rhs)

        # Info (run number, frame number, exposure time)
        self.globals.info = w.InfoFrame(lhs)

        # container frame for switch options (observe, fps and setup)
        topLhsFrame = tk.Frame(lhs)
        self.globals.fpslide = SlideFrame(topLhsFrame, show_mimic=False)
        self.globals.tecs = CCDTempFrame(topLhsFrame)
        self.globals.setup = w.InstSetup(topLhsFrame)
        self.globals.observe = hcam.Observe(topLhsFrame)

        # counts and S/N frame
        self.globals.count = hcam.CountsFrame(lhs)

        # astronomical info
        self.globals.astro = w.AstroFrame(lhs)

        # CCD temp monitoring
        self.globals.ccd_hw = CCDInfoWidget(self)

        # COMPO control widget
        self.globals.compo_hw = COMPOControlWidget(self)

        # add switcher
        switch = w.Switch(topLhsFrame)
        # pack switch and setup widget
        switch.pack(pady=5, anchor=tk.W)
        self.globals.setup.pack(pady=5, anchor=tk.W)

        # format the LHS
        topLhsFrame.grid(row=0, column=0, sticky=tk.W+tk.N, padx=10, pady=5)
        self.globals.count.grid(row=1, column=0, sticky=tk.W+tk.N, padx=10, pady=5)
        self.globals.astro.grid(row=2, column=0, sticky=tk.W+tk.N, padx=10, pady=5)
        self.globals.info.grid(row=3, column=0, sticky=tk.W+tk.N, padx=10, pady=5)

        # format the RHS
        self.globals.ipars.grid(row=0, column=1, sticky=tk.W+tk.N, padx=10, pady=5)
        self.globals.rpars.grid(row=1, column=1, sticky=tk.W+tk.N, padx=10, pady=5)

        # format the bottom
        self.globals.clog.grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
        self.globals.rlog.grid(row=0, column=1, sticky=tk.W, padx=10, pady=5)

        # now add the LHS, RHS and bottom frames
        lhs.grid(row=0, column=0, sticky=tk.W+tk.N, padx=5, pady=5)
        rhs.grid(row=0, column=1, sticky=tk.W+tk.N, padx=5, pady=5)
        bottom.grid(row=1, column=0, columnspan=2,
                    sticky=tk.W+tk.N, padx=5, pady=5)

        # menubar. 'Quit', configuration settings
        menubar = tk.Menu(self)
        menubar.add_command(label='Quit', command=self.ask_quit)

        # settings menu in menubar
        settingsMenu = tk.Menu(menubar, tearoff=0)

        # expert settingsMenu
        expertMenu = w.ExpertMenu(settingsMenu, self.globals.observe, self.globals.setup,
                                  self.globals.ipars, self.globals.rpars,
                                  self.globals.fpslide, switch)
        settingsMenu.add_cascade(label='Expert', menu=expertMenu)

        # telescope chooser
        telChooser = w.TelChooser(settingsMenu, self.globals)
        settingsMenu.add_cascade(label='Telescope', menu=telChooser)

        # boolean switches
        settingsMenu.add_checkbutton(label='Require run params',
                                     var=w.Boolean(self, 'require_run_params'))
        settingsMenu.add_checkbutton(label='Confirm target name change',
                                     var=w.Boolean(self, 'confirm_on_change'))
        settingsMenu.add_checkbutton(label='Rtplot server on',
                                     var=w.Boolean(self, 'rtplot_server_on'))
        settingsMenu.add_checkbutton(label='Server on',
                                     var=w.Boolean(self, 'hcam_server_on'))
        settingsMenu.add_checkbutton(label='Focal plane slide on',
                                     var=w.Boolean(self, 'focal_plane_slide_on'))
        settingsMenu.add_checkbutton(label='TCS checking on',
                                     var=w.Boolean(self, 'tcs_on'))
        # we add a callback to this one to enable start button if appropriate
        settingsMenu.add_checkbutton(label='Assume NGC server online',
                                     var=w.Boolean(self, 'eso_server_online',
                                                   lambda flag: (self.globals.ipars.check() if
                                                                 flag else None))
                                     )

        # now we enable/disable this last item according to expert status
        lindex = settingsMenu.index(tk.END)
        if self.globals.cpars['expert_level']:
            settingsMenu.entryconfig(lindex, state=tk.NORMAL)
        else:
            settingsMenu.entryconfig(lindex, state=tk.DISABLED)
        expertMenu.indices = [lindex]

        # add to menubar
        menubar.add_cascade(label='Settings', menu=settingsMenu)

        # add a menubar link to show the hardware window
        hwMenu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label='CCD Status', menu=hwMenu)
        hwMenu.add_command(label='Show...', command=self.globals.ccd_hw.deiconify)
        hwMenu.add_checkbutton(label='CCD temperature monitoring',
                               var=w.Boolean(self, 'ccd_temp_monitoring_on'))
        hwMenu.add_checkbutton(label='Flow rate monitoring',
                               var=w.Boolean(self, 'flow_monitoring_on'))
        hwMenu.add_checkbutton(label='Pressure monitoring',
                               var=w.Boolean(self, 'ccd_vac_monitoring_on'))
        label = '{} temperature monitoring'.format(
            'Chiller' if self.globals.cpars['telins_name'].lower == 'wht' else 'Rack'
        )
        hwMenu.add_checkbutton(label=label,
                               var=w.Boolean(self, 'chiller_temp_monitoring_on'))

        # stick the menubar in place
        self.config(menu=menubar)

        # all the components are defined. Let's try and load previous application settings
        app_file = os.path.join(os.path.expanduser('~/.hdriver'), 'app.json')
        if os.path.isfile(app_file):
            json_string = open(app_file).read()
            try:
                self.globals.ipars.loadJSON(json_string)
                self.globals.rpars.loadJSON(json_string)
                self.globals.clog.info('Loaded saved instrument and run settings')
            except Exception as err:
                self.globals.clog.warn('Failed to load saved settings')
                self.globals.clog.warn(str(err))

        # configure
        self.title('hdriver')
        self.protocol("WM_DELETE_WINDOW", self.ask_quit)
        self.createcommand("::tk::mac::Quit", self.ask_quit)

        # run checks
        self.globals.ipars.check()

        # check application directories
        check_user_dir(self.globals)

        # rtplot server. will be enabled by "check" if enabled
        self.server = None

        # File logging
        if self.globals.cpars['file_logging_on']:
            # bizarrely, file dialog does not close on OS X
            # the updating of the root Tk object
            # fixes this.
            self.check()
            # self.withdraw()  # hide main window
            self.globals.logfile = filedialog.asksaveasfilename(
                initialdir=self.globals.cpars['log_file_directory'],
                defaultextension='.log',
                filetypes=[('log files', '.log'), ],
                title='Name of log file'
            )
            # self.deiconify()  # reveal main window
            if self.globals.logfile:
                self.globals.clog.update(self.globals.logfile)
                self.globals.rlog.update(self.globals.logfile)
            else:
                self.globals.clog.info('Will not log to file')
        else:
            self.globals.clog.warn('Logging to a file is disabled')

        # update
        self.check()

    @inlineCallbacks
    def ask_quit(self):
        """
        Shutdown routine
        """
        if not messagebox.askokcancel('Quit', 'Really quit hdriver?'):
            print('OK - I will not quit')
        else:
            bring_offline = True
            try:
                online = yield isOnline(self.globals)
                powered_on = yield isPoweredOn(self.globals)
            except:
                # failure to find status probably means the server is
                # down, so there's no point trying to power off
                online = False
                powered_on = False

            # explicity check online, since cpars is false on start
            if (online and powered_on and
                    not messagebox.askyesno('Power Off', 'Turn off CCD clocks?')):
                bring_offline = False

            # save config
            write_config(self.globals)
            dump_app(self.globals)

            if bring_offline:
                # attempt to power off server
                try:
                    result = yield execCommand(self.globals, 'ngc_server.offline')
                except:
                    result = False
                if result:
                    self.globals.clog.info('ESO server idle and child processes quit')
                else:
                    self.globals.clog.warn('Power off failed')

            try:
                self.after_cancel(self._after_id)
                self.after_cancel(self.globals.astro.after_id)
                self.after_cancel(self.globals.ccd_hw.after_id)
                self.quit()
            except Exception:
                pass

            reactor.stop()
            tksupport.uninstall()

            # be nice on exit
            print('\nThank you for using hdriver')

    @inlineCallbacks
    def on_wamp_session(self, session):
        """
        called when joining a WAMP session
        """
        self.globals.clog.info('joined WAMP session')
        self.globals.session = session

        # subscribe to ALL THE TELEMETRY TOPICS HERE
        yield session.subscribe(self.globals.fpslide.on_telemetry, "hipercam.slide.telemetry")
        yield session.subscribe(self.globals.compo_hw.on_telemetry, "hipercam.compo.telemetry")
        subscription_needed = [self.globals.tecs, self.globals.info, self.globals.setup,
                               self.globals.ccd_hw, self.globals.observe]

        for widget in subscription_needed:
            for topic, callback in widget.telemetry_topics:
                yield session.subscribe(callback, topic)

    def check(self):
        """
        Run regular checks of FIFO queue which stores exceptions raised in threads

        Also check to see if we should start rtplot server
        """
        try:
            exc = self.globals.FIFO.get(block=False)
        except queue.Empty:
            pass
        else:
            name, error, tback = exc
            self.globals.clog.warn('Error in thread {}: {}'.format(name, error))
            self.globals.clog.debug(tback)

        if self.server is None and self.globals.cpars['rtplot_server_on']:
            self.startRtplotServer()
        elif self.server is not None and not self.globals.cpars['rtplot_server_on']:
            print('shutting down rtplot server')
            self.server.stopListening()
            self.server = None

        # schedule next check
        self._after_id = self.after(2000, self.check)

    def startRtplotServer(self):
        """
        Starts up the server to handle GET requests from rtplot
        It is at this point that we pass the window parameters
        to the server.
        """
        factory = RtplotFactory(self.globals.ipars, self.globals)
        self.server = reactor.listenTCP(self.globals.cpars['rtplot_server_port'],
                                        factory)


class WampComponent(ApplicationSession):
    def onJoin(self, details):
        gui = self.config.extra['gui']
        gui.on_wamp_session(self)


if __name__ == "__main__":

    # command-line parameters
    parser = argparse.ArgumentParser(description=usage)

    # The main window.
    gui = GUI()
    tksupport.install(gui)
    component = Component(transports='ws://localhost:8080/ws',
                          realm='realm1', session_factory=WampComponent,
                          extra={'gui': gui})
    run([component], log_level=None)
