#!/usr/bin/env python3

# The best way to start this server is from the parent folder using:
#   python3 -m stage.director --quiet

cmd_desc = "Show controller script to start and stop individual performances."

import argparse
import time
import math
import logging
import threading
import subprocess
import signal

import numpy as np

# import the pythonosc package
from pythonosc import dispatcher
from pythonosc import osc_server

# OSC port for lighting commands
import stage.config
import stage.network

# library schedule
import stage.HuntLibrary as HuntLibrary

# import common logging functions
from .logconfig import add_logging_args, open_log_file, configure_logging

# initialize logging for this module
log = logging.getLogger('director')

#================================================================
# Define the set of available performances.  Each is a script which
# runs for a finite duration.

shows = (
    # working-directory, script-path, optional-args (e.g. file path)
    {'cwd': None, 'script': "demo-lighting.py", 'opt': []},
    {'cwd': None, 'script': "demo-arm.py", 'opt': []},
    {'cwd': None, 'script': "demo-boom-wheel.py", 'opt': []},

    # {'cwd': "works", 'script': "window1.py",     'opt': []},
    # {'cwd': "works", 'script': "window2.py",     'opt': []},
    # {'cwd': "works", 'script': "window3.py",     'opt': []},
    # {'cwd': "works", 'script': "window4.py",     'opt': []},
    # {'cwd': "works", 'script': "suspension-1-pink-surprise.py", 'opt': []},
    # {'cwd': "works", 'script': "suspension-2-lungs.py",         'opt': []},
    # {'cwd': "works", 'script': "suspension-3-leaf.py",          'opt': []},
    # { 'cwd': "works", 'script': "suspension-4-jellyfish.py",     'opt': []},
    # {'cwd': "works", 'script': "suspension-5-elevator.py",      'opt': []},
    # {'cwd': "works", 'script': "suspension-all.py",             'opt': []},
    )

#================================================================
class Director:
    def __init__(self, args, demo=False):

        # Start without any child performance process.
        self.performance = None

        # True if running in 'demo' mode without schedule.
        self.demo_mode = demo

        # Create the networking listener
        self.init_networking_input(args)

        # Create the networking senders
        self.network = stage.network.TheaterNetwork(args)

        # Counter to cycle through the available shows.
        self.next_show = 0

        return

    #---------------------------------------------------------------
    def close(self):
        log.info("Closing ports.")
        self.server.shutdown()

    #---------------------------------------------------------------
    def init_networking_input(self, args):

        # Initialize the OSC message dispatch system.
        self.dispatch = dispatcher.Dispatcher()
        self.dispatch.map("/audience",   self.audience)
        self.dispatch.set_default_handler(self.unknown_message)

        # Start and run the server.
        udp_port = stage.config.director_UDP_port
        listen_IP = '0.0.0.0'
        self.server = osc_server.OSCUDPServer((listen_IP, udp_port), self.dispatch)
        self.server_thread = threading.Thread(target=self.server.serve_forever)
        self.server_thread.daemon = True
        self.server_thread.start()
        log.info("started OSC server on port %s:%d", listen_IP, udp_port)

    def unknown_message(self, msgaddr, *args):
        """Default handler for unrecognized OSC messages."""
        log.warning("Received unmapped OSC message %s: %s", msgaddr, args)

    def audience(self, msgaddr, *args):
        """Process /audience message received via OSC over UDP."""
        # log.debug("received OSC message %s: %s", msgaddr, args)
        if self.performance is None:
            log.debug("Running idle performance.")
            self.performance = subprocess.Popen(["./idle-show.py", "--debug"])

    #---------------------------------------------------------------
    def start_show(self):
        if self.performance is None:
            show = shows[self.next_show % len(shows)]
            self.next_show += 1

            common_options = ["--debug"]
            # common_options += ["--console", "--verbose"]  # for interactive testing
            # common_options += ["--ip", "127.0.0.1"]       # for offline testing

            args = ["python3", show['script']] + common_options + show['opt']

            log.debug("Run performance %d: %s", self.next_show, args)
            self.performance = subprocess.Popen(args, cwd=show['cwd'])

    #---------------------------------------------------------------
    # Event loop.
    def run(self):
        """Run a performance event loop to execute periodic state updates."""

        while True:
            if self.performance is not None:
                returncode = self.performance.poll()
                if returncode is None:
                    log.debug("Waiting for performance to finish.")
                    time.sleep(5)

                else:
                    log.debug("Performance finished.")
                    self.performance = None
                    time.sleep(2)

            else:
                if self.demo_mode:
                    log.info("Starting demo iteration.")
                    self.start_show()

                elif HuntLibrary.current_time_properties().get('is_show_time'):
                    log.info("Starting scheduled iteration.")
                    self.start_show()

                else:
                    log.debug("Sitting idle.")
                    time.sleep(20)

#================================================================
# The following section is run when this is loaded as a script.
if __name__ == "__main__":

    # set up logging
    open_log_file('logs/director.log')
    log.info("Starting director.py")
    np.set_printoptions(linewidth=np.inf)

    # Initialize the command parser.
    parser = argparse.ArgumentParser(description = cmd_desc)
    parser.add_argument("--ip", default=stage.config.theater_IP,  help="IP address of the OSC receivers (default: %(default)s).")
    parser.add_argument("--demo", action='store_true',  help="Enable continuous operation without schedule constraints.")

    add_logging_args(parser)

    # Parse the command line, returning a Namespace.
    args = parser.parse_args()

    # Modify logging settings as per common arguments.
    configure_logging(args)

    # Create the performance controller.
    director = Director(args, args.demo)

    # Begin the performance.  This may be safely interrupted by the user pressing Control-C.
    try:
        director.run()

    except KeyboardInterrupt:
        log.info("User interrupted operation.")
        print("User interrupt, shutting down.")
        director.close()

    log.info("Exiting director.py")
