#!/usr/bin/env ruby
# frozen_string_literal: true

##
# Given a path to a file holding a list of events as generated by the
# `upwatch` tool, convert the events found in the file into a diagram
# visualizing their sequencing. The result is written to stdout. The
# diagram/image format is SVG (scalable vector graphics).

# Usage:
# sequence /path/to/event/file > /path/to/svg/result

ROW_HEIGHT = 20

def main
  emit(*(process ingest cmdline))
end

def cmdline
  usage unless ARGV.length == 1
  ARGV.first
end

def usage
  warn 'Usage: sequence /path/to/event/file'
  exit 1
end

def ingest(path)
  File.read(path).split("\n")
end

# A singluar event, as read from the input file.
Event = Struct.new(
  :pod,    # The name of the pod
  :state,  # The state of the event
  :stamp,  # Timestamp of the event, in seconds since epoch
  :offset, # Timestamp of the event, in seconds since start of run
  keyword_init: true
)

# Parse the input file, returning a list of events.
def read_events(lines)
  # Each line has 4 fields:
  # - The word "change"
  # - A timestamp, as seconds from epoch
  # - The state of a pod (e.g. "Pending", "Init:0/1", "Run:1/2", ...)
  # - The name of the pod.
  # For example: "change 1596841778 Ready uaa-0"

  entries = lines.map do |line|
    (marker, stamp, state, pod) = line.split
    Event.new(stamp: stamp.to_i, state: state, pod: pod) if marker == 'change'
  end
  base = entries.map(&:stamp).min
  entries.each { |entry| entry.offset = entry.stamp - base }
end

# Group events by the pod name, and sort them by ending time (using the pod name
# as a tie-breaker).
def sort_events(all_events)
  all_events.group_by(&:pod).sort_by { |name, ev| [ev.last.stamp, name] }
end

# An Action desribes one rectangle to be drawn in the graph
Action = Struct.new(
  :state,          # The state this action represents, e.g. "Completed"
  :start,          # A Time that this action started at
  :x, :y,          # Top left corner of this action
  :width, :height, # Size of the rectangle
  keyword_init: true
)

def process(lines)
  all_events = sort_events(read_events(lines))

  # Generate the diagram data, a map of pod name -> list(action)
  diagram = all_events.each_with_index.map do |(pod_name, events), index|
    # Add a fake event for closure of true last event.
    events << Event.new(
      stamp: events.last.stamp + 5,
      offset: events.last.offset + 5
    )
    [pod_name, make_actions(events, index, ROW_HEIGHT)]
  end.to_h

  # xmax is the _right_ edge of the last event
  # (or rather, left edge of the fake event)
  xmax = all_events.flat_map(&:last).map(&:offset).max

  [diagram, xmax, all_events.length * ROW_HEIGHT, ROW_HEIGHT]
end

# Compute location and dimensions of the events. The width is
# computed against the following event. Height is fixed. Vertical
# location increments from igroup to igroup.
def make_actions(events, index, height)
  events.each_cons(2).map do |event, next_event|
    Action.new(
      state: event.state,
      start: Time.at(event.stamp),
      x: event.offset,
      y: (index + 1) * height,
      width: next_event.stamp - event.stamp,
      height: height
    )
  end
end

# Emit the SVG graph, given the graph data, dimensions of the graph, and row
# height.
def emit(dia, xmax, ymax, height)
  ehead xmax, ymax, height

  dia.each_pair do |pod_name, actions|
    actions.each do |el|
      title = "#{pod_name}: #{el.state} started @#{el.start} for #{el.width}s"
      ebox text: el.state, x_pos: el.x, y_pos: el.y,
           width: el.width, height: el.height, title: title
    end
    etext pod_name, actions.last.x + actions.last.width + 2, actions.last.y + 2
  end
  etail
end

# Emit the SVG file header
def ehead(xmax, ymax, height)
  xmax += 100        # give space to the (last) text for ig's, to the right
  ymax += 2 * height # give space to the last row, down
  puts %(
    <svg width='#{xmax}' height='#{ymax}' xmlns='https://www.w3.org/2000/svg'>
  )
  ebox text: '<BG>', x_pos: 0, y_pos: 0, width: xmax, height: ymax
end

# Emit some text at the given position (given as the top-left corner).
def etext(text, x_pos, y_pos)
  puts %(
    <text
      x='#{x_pos}' y='#{y_pos}'
      style='dominant-baseline: text-before-edge;'
    >#{text}</text>
  )
end

# Emit the SVG file trailer
def etail
  puts '</svg>'
end

# ebox emits a box at (x, y)+(width, height) with color based on the text
def ebox(text:, x_pos:, y_pos:, width:, height:, title: '')
  puts %(
    <rect
      x="#{x_pos}" y="#{y_pos}" width="#{width}" height="#{height}"
      style="fill: #{color text}; stroke: black;">
        <title>#{title}</title>
    </rect>
  )
end

def color(text)
  case text
  when '<BG>'                                            then 'white'
  when 'Error'                                           then 'red'
  when /(?:Init:)?CrashLoopBackOff|Terminating/          then 'orange'
  when /^Init:/                                          then 'lightblue'
  when /Pending|PodInitializing|ContainerCreating|^Run:/ then 'yellow'
  when 'Ready', 'Completed'                              then 'green'
  end
end

main
