#!/usr/bin/python
#
# Dump the contents of the schedule in a plottable form
#

# General syntax:
#     pscheduler plot-schedule [options]

import datetime
import optparse
import os
import pscheduler
import pytz
import sys


pscheduler.set_graceful_exit()


#
# Utilities
#

def get_time_with_delta(string):
    """
    Get an absolute time or delta and return an ISO8601 string with
    the absolute time.
    """

    # If it looks like an ISO time, return that.
    try:
        absolute = pscheduler.iso8601_as_datetime(string)
        # Default behavior is to localize naive times.
        if absolute.tzinfo is None:
            absolute = pytz.utc.localize(absolute)
            return pscheduler.datetime_as_iso8601(absolute)
    except ValueError:
        pass

    try:
        if string[0:1] == "+P":
            delta = pscheduler.iso8601_as_timedelta(string[1:])
        elif string[0:1] == "-P":
            delta = -1 * pscheduler.iso8601_as_timedelta(string[1:])
        else:
            pass
    except ValueError:
        pscheduler.fail("Invalid time delta '%s'" % (string))

    # Let this throw what it's going to throw.
    delta = pscheduler.iso8601_as_timedelta(string)

    return pscheduler.datetime_as_iso8601(
        pscheduler.time_now() + delta)


#
# Gargle the arguments
#

whoami = os.path.basename(sys.argv[0])
args = sys.argv[1:]


# Pre-convert any trailing arguments that look like times or deltas
# into raw times so the option parser doesn't choke on negative
# deltas.

if len(args) > 0:
    arg = -1
    while True and abs(arg) <= len(args):
        try:
            args[arg] = (get_time_with_delta(str(args[arg])))
        except ValueError:
            break
        arg -= 1



class VerbatimParser(optparse.OptionParser):
    def format_epilog(self, formatter):
        return self.epilog

opt_parser = VerbatimParser(
    usage="Usage: %prog [ OPTIONS ] [ delta | start end ] ",
    epilog=
"""
Example:

  plot-schedule
      Plot the schedule on the local host for the next hour

  plot-schedule --host ps3.example.net
      Plot the schedule on ps3.example.net for the next hour

  plot-schedule -PT1H
      Plot the schedule on the local host for an hour in the
      the past

  plot-schedule +PT25M
      Plot the schedule on the local host for 25 minutes in
      the future

  schedule -PT1H +PT30M
      Plot the schedule on the local host for an hour in the
      the past and 30 minutes into the future

  schedule 2016-05-01T12:40:00 2016-05-01T12:55:00
      Plot the schedule on the local host between the times
      specified.
"""
    )
opt_parser.disable_interspersed_args()

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("--host",
                      help="Request schedule from named host",
                      default=None,
                      action="store", type="string",
                      dest="host")

(options, remaining_args) = opt_parser.parse_args(args)



full_host = options.host if options.host is not None \
            else pscheduler.api_this_host()


# This outputs PNG, which isn't suitable for TTYs.

if sys.stdout.isatty():
    pscheduler.fail("Not sending PNG output to a tty.  Consider redirecting.")



now = pscheduler.time_now()

if len(remaining_args) == 0:

    # Default; show an hour's worth.
    start = now
    end = start + datetime.timedelta(hours=1)

elif len(remaining_args) == 1:

    # One argument is an absolute time or a timedelta.

    try:
        arg = pscheduler.iso8601_as_datetime(remaining_args[0])
    except ValueError:
        pscheduler.fail("Invalid time specification")

    if arg < now:
        start = arg
        end = now
    else:
        start = now
        end = arg

elif len(remaining_args) == 2:

    try:
        start = pscheduler.iso8601_as_datetime(remaining_args[0])
        end = pscheduler.iso8601_as_datetime(remaining_args[1])
    except ValueError:
        pscheduler.fail("Invalid time specification")


    if end < start:
        start, end = end, start

else:
    pscheduler.fail(usage)


#
# Fetch the schedule
#

status, schedule = pscheduler.url_get(
    pscheduler.api_url(host=options.host, path="schedule"),
    params={
        "start": pscheduler.datetime_as_iso8601(start),
        "end": pscheduler.datetime_as_iso8601(end)
        },
    bind=options.bind,
    throw = False
    )

if status != 200:
    pscheduler.fail("Server returned status %d: %s" % (status, schedule))



script_lines = [
"""
reset

set terminal png notransparent truecolor size 800,1200 background rgb "#ffffff"

set timefmt "%Y-%m-%dT%H:%M:%S"

unset xtics
set x2label "Scheduling"
set x2range [0.5:4.5]
set x2tics out scale 0 ( \\
    "" 0, \\
    "Exclusive" 1, \\
    "Normal" 2, \\
    "Background" 3, \\
    "Non-Start" 4 \\
    )


set ylabel "Run Time"
set ydata time
set ytics out nomirror
set format y "%Y-%m-%d\\n%H:%M:%S"
""",
    """set yrange ["%s":"%s"] reverse""" \
    % (pscheduler.datetime_as_iso8601(start),
       pscheduler.datetime_as_iso8601(end)),
    """set title "pScheduler Schedule for %s""" % (full_host),
"""
set key off
set grid front noxtics ytics mytics linetype 0
set boxwidth 0.9

set style fill solid border lc rgb "#000000"

plot "-" using 1:2:3:2:3 \\
  with candlesticks \\
  linewidth 1 \\
  lc rgb "#00e000" \\
  axes x2y1"""
]

#
# Dump it out
#

class_levels = {
    "exclusive": 1,
    "normal": 2,
    "background-multi": 3,
    "background": 3,
    "nonstart": 4,
}


if len(schedule) == 0:
    pscheduler.fail("Nothing scheduled %s to %s" % (
        pscheduler.datetime_as_iso8601(start),
        pscheduler.datetime_as_iso8601(end)
        ))


for run in schedule:

    try:
        run_state = run["state"]
        sched_class = run_state if run_state in [ "nonstart" ] \
                      else run["test"]["scheduling-class"]
    except KeyError:
        pscheduler.fail("%s: Software too old to support this feature" \
                        % (host))

    script_lines.append(
        "%s %s %s" % (
            class_levels[sched_class],
            run["start-time"],
            run["end-time"]
            )
    )

script_lines.append("eof")


#
# Push it through GNUPlot to make a PNG
#

status, out, err = pscheduler.run_program(
    "gnuplot",
    stdin="\n".join(script_lines)
    )

if status != 0:
    pscheduler.fail(err)

if sys.stderr.isatty():
    sys.stderr.write("Plotted %d runs\n" % (len(schedule)))


pscheduler.succeed(out)
