#!/usr/bin/python
#
# Monitor pScheduler's Schedule
#


import curses
import datetime
import optparse
import pscheduler
import time

#
# Gargle the arguments
#

usage = "Usage: %prog [options]"
opt_parser = optparse.OptionParser(usage = usage)
opt_parser.disable_interspersed_args()

# GENERAL OPTIONS

opt_parser.add_option("--bind",
                      help="Make the request from the provided address",
                      default=None,
                      action="store", type="string",
                      dest="bind")

opt_parser.add_option("--past",
                      help="Percentage of available screen lines to occupy with past runs (0..100)",
                      action="store", type="int",
                      default=25,
                      dest="past")

opt_parser.add_option("--host",
                      help="pScheduler host",
                      action="store", type="string",
                      default=pscheduler.api_this_host(),
                      dest="host")

opt_parser.add_option("--refresh",
                      help="Seconds between refreshes of the schedule data",
                      action="store", type="int",
                      default=3,
                      dest="refresh")


(options, remaining_args) = opt_parser.parse_args()

if len(remaining_args) != 0:
    opt_parser.print_usage()
    pscheduler.fail()

if options.past < 0 or options.past > 100:
    pscheduler.fail("Invalid --past; must be in 0..100")
past_fraction = float(options.past) / 100.0


# TODO: Make these options
if options.refresh < 0:
    pscheduler.fail("Invalid --refresh; must be > 0")
update_interval = datetime.timedelta(seconds=options.refresh)



#
# Some utilities
#


def hline(screen, line):
    height, width = screen.getmaxyx()
    screen.hline(line, 0, curses.ACS_HLINE, width)

def center_on_line(screen, line, string):
    height, width = screen.getmaxyx()
    screen.addstr(line, (width - len(string))/2, string)

def draw_static(screen):
    height, width = screen.getmaxyx()
    screen.clear()
    center_on_line(screen, 0, "pScheduler Monitor")
    screen.addstr(0, width-len(options.host), options.host)
    hline(screen, 1)
    screen.refresh()


def update_time(screen):
    time_str = pscheduler.datetime_as_iso8601(pscheduler.time_now())
    screen.addstr(0, 0, time_str)
    screen.refresh()



def update_tasks(screen):

    height, width = screen.getmaxyx()

    # Put something in the corner to show we're busy
    loading_str = "   [LOADING]"
    screen.addstr(0, width-len(loading_str), loading_str)
    screen.refresh()

    status, schedule = pscheduler.url_get(
        pscheduler.api_url(host=options.host, path="/monitor"),
        params={ "window": height },
        bind=options.bind,
        throw=False)

    # Easy outs

    error_lines = []

    if status != 200:
        error_lines = [ "Unable to read schedule from server.",
                        "",
                        "Error code %d" % status,
                        "",
                        schedule.replace("\n", "  ")
                        ]
    elif len(schedule) == 0:
        error_lines = [ "Schedule is empty." ]

    if error_lines:
        screen.clear()
        line = (height - len(error_lines)) / 2
        for text in error_lines:
            if len(text) > width:
                text = "%s%s" % (text[:width-3], "...")
            center_on_line(screen, line, text)
            line += 1
        screen.refresh()
        return


    # Sort the schedule into bins (what was, what is, what shall be)

    past = []
    present = []
    future = []

    for run in schedule:
        ppf = run["ppf"]
        if ppf < 0:
            past.append(run)
        elif ppf == 0:
            present.append(run)
        else:
            future.append(run)


    # Dole out screen real estate.

    # Curses uses zero-based geometry.
    rows_left = height
    last_row = height - 1

    # Things running take top priority
    if len(present) > rows_left:
        present = present[:rows_left]
    rows_left -= len(present)
    assert rows_left >= 0, "Left = %d" % rows_left

    # Past things take up to some fraction of what's left.
    if rows_left > 0:
        past_max = int(rows_left * past_fraction)
        if len(past) > past_max:
            past = past[-past_max:]
        rows_left -= len(past)
    else:
        past = []
    assert rows_left >= 0, "Left = %d" % rows_left

    # Things in the future get whatever's left.
    if rows_left > 0:
        if len(future) > rows_left:
            future = future[:rows_left]
        rows_left -= len(future)
    else:
        future = []
    assert rows_left >= 0, "Left = %d" % rows_left


    show_schedule = past
    if len(past) > 0:
        show_schedule[-1]["delimit"] = True
    show_schedule.extend(present)
    if len(present) > 0:
        show_schedule[-1]["delimit"] = True
    show_schedule.extend(future)

    assert len(show_schedule) <= height

    screen.clear()

    row = 0
    for run in show_schedule:

        cli = [ run["task"]["test"]["type"] ]
        try:
            cli.extend(run["cli"])
        except KeyError:
            cli.append("(CLI not available)")

        # Pad the string out to the width so inverse shows properly.
        line = "%s %-12s %s" % (run["start-time"],
                                run["state-display"], " ".join(cli))
        line = "%s%s" % (line, " " * (width-len(line)))

        if len(line) > width:
            line = "%s%s" % (line[:width-1], "+")

        state = run["state"]

        if state in [ "pending", "on-deck" ]:
            # TODO: Color default
            line_attr = curses.A_NORMAL
        elif state in [ "running", "cleanup" ]:
            # TODO: Color default
            line_attr = curses.A_REVERSE
        elif state in [ "overdue", "missed", "failed" ]:
            # TODO: Color red
            line_attr = curses.A_BOLD
        elif state in [ "preempted" ]:
            # TODO: Color yellow
            line_attr = curses.A_NORMAL
        else:
            line_attr = curses.A_NORMAL

        try:
            if run["delimit"]:
                line_attr = line_attr | curses.A_UNDERLINE
        except KeyError:
            pass


        # The last row gets chopped by one character because curses
        # doesn't allow writing to the last character on the screen
        # with addstr.
        if row == last_row:
            line = line[:-1]

        # This is for debugging screen overflow issues
        #line = "Row %d  Height=%d  Left=%d" % (row, height, rows_left)

        screen.addstr(row, 0, line, line_attr)

        row += 1

    screen.refresh()



#
# Main Program
#

try:

    stdscr = curses.initscr()
    stdscr.nodelay(1)
    curses.curs_set(0)
    curses.cbreak()
    curses.noecho()
    task_window = stdscr.subwin(2,0)
  

    draw_static(stdscr)
    update_tasks(task_window)

    last_update = pscheduler.time_now() - update_interval

    while True:

        input = stdscr.getch()

        if input == -1:
            pass
        elif input == curses.KEY_RESIZE:
            stdscr.clear()
            task_window = stdscr.subwin(2,0)
            draw_static(stdscr)
            # TODO: This causes lots of fetches during resizes.
            update_tasks(task_window)
            continue
        elif chr(input) == " ":
            update_tasks(task_window)
        elif chr(input) in [ "Q", "q", "\033"]:
                break

        update_time(stdscr)
        if pscheduler.time_now() > last_update + update_interval:
            update_tasks(task_window)
            last_update = pscheduler.time_now()

        time.sleep(0.1)

except KeyboardInterrupt:
    pass

finally:
    curses.nocbreak()
    stdscr.keypad(0)
    curses.echo()
    curses.endwin()

pscheduler.succeed()
