"""Multi-camera main application.

Original author: Mark Zvilius, 20-July-2019
For: The Sexton Corp
"""
import sys
import tty
import termios
import tkinter as tk
import select

import E2Protocol
import E2Configs
import ParamFrame
from CameraGroup import CameraGroup
from E2Camera import E2Camera
from Commands import Commands
from BonnetButton import BonnetButton
from VideoWindow import VideoWindow
from BackgroundTasks import BackgroundTasks

##  Video position (upper left corner) and size in pixels.
VIDEO_POSITION = (480,0)
VIDEO_SIZE = (960,540)

##  Background task period, e.g. check for keyboard and button presses.
BACKGROUND_PERIOD_MSEC = 20

button_lookup = {}
key_lookup = {}


def display_error_message(message, display):
    """Show an error message on the graphical and text displays."""
    #
    #   message -- (str) the error message
    #   display -- graphical display
    #
    #   TODO: Perhaps an ErrorMessage class (singleton).
    #

    if display:
        display.update_errmsg(message)
    print(message)


def create_button_lookup(commands):
    """Map from buttons (GPIOs) to commands."""

    global button_lookup

    # These button names must match with the button names in the config file.
    button_name_to_command = {
        'shutter_dn' : commands.shutter_dn,
        'shutter_up' : commands.shutter_up,
        'wb_dn' : commands.wb_dn,
        'wb_up' : commands.wb_up,
        'iso_dn' : commands.iso_dn,
        'iso_up' : commands.iso_up,
        'cam_dn' : commands.cam_dn,
        'cam_up' : commands.cam_up,
        'fstop' : commands.fstop_toggle,
        'encoder' : commands.encoder_toggle,
        'record' : commands.record_toggle
        }

    # Throws an exception if a name in button_map is not found in
    # button_name_to_command.
    for (locn,name) in E2Configs.button_map.items():
        button_lookup[locn] = button_name_to_command[name]


def create_key_lookup(commands):
    """Map from keypresses to commands."""

    global key_lookup
    key_lookup['c'] = commands.cam_up
    key_lookup['C'] = commands.cam_dn
    key_lookup['w'] = commands.wb_up
    key_lookup['W'] = commands.wb_dn
    key_lookup['i'] = commands.iso_up
    key_lookup['I'] = commands.iso_dn
    key_lookup['s'] = commands.shutter_up
    key_lookup['S'] = commands.shutter_dn
    key_lookup['f'] = commands.fstop_toggle
    key_lookup['e'] = commands.encoder_toggle
    key_lookup['R'] = commands.record_toggle
    key_lookup['h'] = text_display_header
    key_lookup['-'] = commands.stop_video
    key_lookup['='] = commands.start_video


def create_command_lookups(commands):
    """Create button_lookup and key_lookup."""
    create_button_lookup(commands)
    create_key_lookup(commands)

##
#   Set current parameter values on the graphical display.
#
#   @param  display     Parameter display.
#
#   TODO: improve .. maybe have the display provide a dictionary of handlers
#   What I want is that anytime a camera parameter changes, trigger a display update
#   (and read the param back from the camera).
#
def init_graphical_display(display):
    if display is None:
        return

    for (k,v) in E2Configs.camera_settings.items():
        if k == E2Protocol.KEY_ISO:
            display.update_iso(v)
        elif k == E2Protocol.KEY_WB:
            display.update_wb(v)
        elif k == E2Protocol.KEY_SHUTTER:
            display.update_shutter(v)
        elif k == E2Protocol.KEY_ENCODER:
            display.update_encoder(v)
        elif k == E2Protocol.KEY_IRIS:
            display.update_fstop(v)
        elif k == E2Protocol.KEY_PROJECT_FPS:
            display.update_fps(v)

    display.update_recording(False)
    display.clear_errmsg()

def text_display_header():
    """Print a header for the text display."""

    print('c/C -- camera up/down')
    print('w/W -- white balance up/down')
    print('i/I -- iso up/down')
    print('s/S -- shutter up/down')
    print(' e  -- encoder change')
    print(' f  -- f-stop change')
    print(' R  -- record start/stop')
    print(' -  -- stop video (for testing)')
    print(' =  -- restart video (for testing)')
    print('ESC -- exit program')

    print()
    E2Configs.print_settings()


def process_key(k):
    """Look key up in key_lookup and call the handler.

    k -- key from stdin
    returns -- if app should exit
    """
    if ord(k) == 0x1b:      # esc to exit
        return True
    
    if k in key_lookup:
        key_lookup[k]()
    return False


def poll_keys():
    """Poll for keypresses, execute handler."""
    should_exit = False
    while not should_exit:
        r = select.select([sys.stdin],[],[],0.0)[0]
        if not r:
            break
        should_exit = process_key(sys.stdin.read(1))
    return should_exit


def background_loop(root_window, tasks):
    """Background tasks.

    Check for button presses and process.
    Check for keyboard presses (for dev and testing purposes).
    Perform once-per-second tasks.
    TODO: This does not work if there is no root window. Maybe that's ok?

    root_window -- Root window.
    tasks -- BackgroundTasks object.
    """
    BonnetButton.poll()
    should_exit = poll_keys()
    if should_exit and root_window:
        root_window.destroy()
        return

    # Once-per-second tasks.
    tasks.poll()

    if root_window:
        root_window.after(BACKGROUND_PERIOD_MSEC, background_loop, root_window, tasks)


#-----------------------------------------------------#
#   Here begins the Sexton Multi-camera Application   #
#-----------------------------------------------------#
if __name__ == '__main__':
    #
    # TODO: Need to improve error handling so that errors
    # during these initialization steps can be reported on
    # the graphical window.
    #

    # Create the window for the camera settings display.
    # If display creation fails (probably due to X-windows not available), set
    # root=None and continue. This allows testing with only a terminal window.
    root = None
    param_frame = None
    try:
        root = tk.Tk()
    except tk.TclError as err:
        print()                         # some output from Tk doesn't have newline
        print('TclError', err)
        print('no graphical display')
    else:
        param_frame = ParamFrame.run_ui(root)

    # Read the config files.
    # An exception is fatal.
    try:
        E2Configs.load()
    except:
        print('failed to load config files:', sys.exc_info()[1])
        exit(1)

    # Check for no-lens (testing only).
    no_lens = E2Configs.check_no_lens_config()

    # Initialize the graphical and text displays.
    init_graphical_display(param_frame)
    text_display_header()

    # Initialize the cameras.
    # If a camera fails to start, the app continues with a subset, or
    # even with no cameras.
    cams = CameraGroup(E2Configs.camera_ips, no_lens)
    try:
        cams.start_cameras()
    except CameraGroup.ConfigError as err:
        # This happens if no cameras are configured.
        display_error_message(str(err), param_frame)
    except CameraGroup.ConnectionError as err:
        display_error_message(str(err), param_frame)

    # Start video display using camera 0.
    video = VideoWindow(size=VIDEO_SIZE, position=VIDEO_POSITION, cameras=cams)
#    video.set_streaming_args(E2Camera.OMXPLAYER_ARGS)
#    camera_id = video.go(0)
#    param_frame.update_camera(camera_id)

    # Instantiate Commands object and create command lookup tables.
    commands = Commands(cameras=cams, display=param_frame, video=video)
    create_command_lookups(commands)

    # Initialize the buttons.
    # If the hardware does not exist, presumably the user has a keyboard.
    # TODO: put messages on graphical display as well.
    try:
        BonnetButton.init_buttons(button_lookup=button_lookup)
    except BonnetButton.HardwareError as err:
        print('button init failed: {0}'.format(err))
        print('use keyboard instead')

    # Initialize once-per-second background tasks.
    background_tasks = BackgroundTasks(commands)

    # To process key-by-key need to set stdin to cbreak mode.
    # Since this happens at the terminal driver, must be sure to
    # switch back before exiting.

    # TODO: This does not work without a root window (console-only) because
    # background_loop() is implemented by using after() from Tk. To work with
    # console-only would need to implement background_loop using a separate
    # thread.

    orig_settings = termios.tcgetattr(sys.stdin)
    try:
        tty.setcbreak(sys.stdin)
        background_loop(root_window=root, tasks=background_tasks)
        if param_frame:
            param_frame.mainloop()
        # TODO: Need an alternative mainloop if there is no grid:
        #       read buttons and read keyboard.
    finally:
        video.stop()
        termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings)
        print('goodbye')
