#!/usr/bin/env ruby


# 
# Not using metrics yet.


unless 'report' == ARGV[0] || 'tail' == ARGV[0]

  # First arg is ping interval, second is monitor interval.
  pinterval = ARGV[0].to_i if ARGV[0]
  minterval = ARGV[1].to_i if ARGV[1]

  # Defaults are 5 sec and 1 sec respectively.
  pinterval ||= 5
  minterval ||= 1

  # Pool of ping targets.
  TARGETS = [
    'yahoo.com',
    'iana.org',
    'abc7.com',
    'eff.org',
    '100ymm.com',
    'devalot.com',
    'rubylearning.com',
    'facebook.com',
    'sourceforge.org',
    'bitbucket.org',
    'github.org',
    'pastebin.org',
    'deviantart.com',
    'torrentz.eu',
    'tracker.publicbt.com',
    'istole.it',
    '98.tracker.hexagon.cc',
    'pchome.net',
    'tracker.pchome.net',
    #'cpleft.com',
    'tracker.zaerc.com',
    #'tracker.52bt.net',
    'tracker.torrent.to',
    'tracker.sladinki007.net',
    'classiccinemazone.com',
    'linuxtracker.org',
  ]

  # Always abort on exception.
  Thread.abort_on_exception = true

  # List of threads.
  threads = {}
  
  # Current thread.
  ct = Thread.current
  
  # Ping thread.
  threads[:ping] = Thread.new(ct) do |notify|
    loop do
      sleep(pinterval)
      next if Thread.exclusive {notify[:idle]}
      targ = TARGETS[rand * TARGETS.size]
      r = "[#{targ}]" + `ping #{targ} /n 1`.split("\r\n")[3] rescue "[#{targ}]"
      Thread.exclusive {notify[:reply] = r}
    end
  end

  # If the gateway goes down don't bother pinging.
  threads[:gateway] = Thread.new(ct) do |notify|
    loop do
      sleep(1)
      if `ping 10.0.0.1` =~ /timed/
        Thread.exclusive {notify[:idle] = true}
        next
      end
      Thread.exclusive {notify[:idle] = false}
    end
  end

  # Monitor.
  last = nil
  f = File.new('pings.txt', 'a')

  begin
    gw = last = nil
    loop do
      sleep(minterval)
      next if Thread.exclusive {last == ct[:reply]}
      Thread.exclusive {gw, last = ct[:idle], ct[:reply]}
      last =~ /\[(.+)\].+(time=(\d{1,4})ms|timed)/ unless gw
      now = Time.now.inspect; site = $1; time = $3.to_i
      as_str = "#{now}, #{gw ? 'gateway is down' : time}, #{gw ? '' : site}"
      f.puts(as_str); f.flush
      puts as_str if 500 <= time or 0 == time
      as_str = nil
    end
  ensure
    f.close
  end

  threads.each {|l, t| t.join}
  exit(0)

end


unless 'tail' == ARGV[0]
  # 
  # Report section
  #
  
  require 'time'
  
  # Numerics get three(six) time specific methods.
  # self is assumed to hold a number of seconds.
  class Numeric
    def days
      self * 86400
    end
    alias :day :days
    
    def hours
      self * 3600
    end
    alias :hour :hours
    
    def minutes
      self * 60
    end
    alias :minute :minutes
  end
  
  # Makes a symbol, replacing period. Also prefixes possible number start.
  class String
    def mk_sym
      r = self.tr('.', '_')
      r = '_' << r if r[0, 1] =~ /\d/
      return r
    end
  end
  
  # Sum and average for Array. Why aren't these in the core?
  class Array
    def sum
      self.inject(:+)
    end
    def avg
      self.sum.to_f / self.count
    end
  end
  
  # ===Char Mode Graph
  class Graph
    
    attr_accessor :x, :y, :opt
    
    # Musssst give desired size & data... yessssss it mussssst.
    # TODO document options. When you figure out what they are.
    def initialize(x, y, data, opt = {})
      @x, @y, @data, @opt = x, y, data, opt
    end
    
    # Spit out an 'X' by 'Y' graph string.
    def draw
      @scaled, min, max = scale_data(); o = ''
      (@y - 1).downto(0) do |y|
        0.upto(@x - 1) do |x|
          if @scaled[x] >= y then @scaled[x] = -1 if @opt[:no_bars]; o << '*' else o << ' ' end
        end
        o << "\n"
      end
      o << sprintf('Min =%7.2f, Max =%7.2f; used %d points. ', min, max, @data.count)
      o << sprintf('Each point is %5.2f min.', @opt[:time] / @x / 60) if @opt[:time]
      @scaled = nil if @opt[:no_bars]
      o
    end
    
    private
    
    def scale_data
      scale_x = @data.count.to_f / @x; scaled = []
      0.upto(@x - 1) do |x|
        scaled << @data[(x * scale_x)...((x + scale_x) * scale_x)].avg
      end
      min_y, max_y = scaled.min, scaled.max
      scale_y = (max_y - min_y) / @y
      [scaled.map {|n| (n - min_y) / scale_y}, min_y, max_y]
    end
    
  end #cls graph
  
  # Do possible range.
  now = Time.now.to_f
  arg = ARGV[2].to_i if ARGV[2]
  arg ||= 1
  graph_range = case ARGV[1]
    when '-d' then now - arg.days
    when '-h' then now - arg.hours
    when '-m' then now - arg.minutes
    else 0
  end
  
  # Filter data & organize by site.
  raw = File.open('pings.txt', 'r') {|f| f.read}.split("\n").map {|e| e.split(',').map {|n| n.strip}}
  data = Hash.new {|h, k| h[k] = Hash.new(0)}
  raw.each do |ts, tm, st|  #time_stamp, latency, site
    sym = case tm
      when /gateway/ then :gateway_tmo
      when '0'       then :network_tmo
      else st.mk_sym
    end
    h = data[sym]; h[:count] += 1; h[:total] += tm.to_i
  end
  
  # Total time.
  puts
  hours = (Time.parse(raw.last[0]).to_f - Time.parse(raw.first[0]).to_f) / 3600
  printf("%8.2f hours.\n", hours)
  
  # Site stats.
  puts "                  target  count    total average"
  puts "------------------------  -----   ------ -------"
  gt = 0
  data.to_a.sort_by {|k, v| v[:avg] = v[:total].to_f / v[:count].to_f}.each do |k, v|
    gt += v[:count]
    printf("%24s  %5d  %7d  %6.2f\n", k, v[:count], v[:total], v[:avg])
  end
  
  # total data points.
  puts
  puts "#{gt} total data points."
  puts
  STDOUT.flush
  
  # This can be somewhere else. Like put a filter chain in Graph.
  filter = lambda do |data, range| #range is graph_range (time)
    r = data.reject {|e| e[1].index('gateway') or '0' == e[1]} #reject timeouts.
    r = r.select do |e|
      e[0] = Time.parse(e[0]).to_f; e[1] = e[1].to_i # convert.
      e[0] >= range ? e : nil                        # select range.
    end
    [r.last[0] - r.first[0], r.map {|e| e[1]}]
  end
  
  # Filter & graph data.
  time, data = filter.call(raw, graph_range)
  g = Graph.new(79, 25, data)
  g.opt[:time] = time
  #g.opt[:no_bars] = true
  puts g.draw
  exit(0)
end


# 
# tail section.
#
lines = ARGV[1]
lines ||= 10

buf = File.open('pings.txt', 'r') {|f| f.read}.split("\n")
puts buf[(buf.count - lines.to_i)..-1]
exit(0)


# ===All kinds of metrics.
# Theoretically anyway.
module Metrics
  
  require 'time'
  require 'ipaddr'
  
  # ===Network metrics.
  class Net
    
    # Default pool of ping targets.
    @@targets = [
      'yahoo.com',
      'iana.org',
      'abc7.com',
      'eff.org',
      '100ymm.com',
      'devalot.com',
      'rubylearning.com',
      'facebook.com',
      'sourceforge.org',
      'bitbucket.org',
      'github.org',
      'pastebin.org',
      'deviantart.com',
      'torrentz.eu',
      'tracker.publicbt.com',
      'istole.it',
      '98.tracker.hexagon.cc',
      'pchome.net',
      'tracker.pchome.net',
      'cpleft.com',
      'tracker.zaerc.com',
      'tracker.52bt.net',
      'tracker.torrent.to',
      'tracker.sladinki007.net',
      'classiccinemazone.com',
      'linuxtracker.org',
    ]
    
    # Default intervals in seconds.
    DEF_GWP  = 1
    DEF_PING = 5
    DEF_MON  = 1
    
    # Default log name.
    DEF_LOG = 'pings.txt'
    
    # Default gateway.
    DEF_GWA  = '10.0.0.1'  # Default address (yeah I know).
    REGX_GWA = /default gateway.+: ((\d{1,3}\.){3}\d{1,3})/i
    
    # Default maximum size of the crapouts array.
    DEF_MAX_CRAPOUTS = 100
    
    # Find gateway address.
    def self.gateway_addr
      `ipconfig`.each_line {|l| break if l =~ REGX_GWA}
      $1
    end
    
    # Options: (intervals are in seconds).
    # :g_int  = gateway ping interval (default 1)
    # :p_int  = ping interval (default = 5)
    # :m_int  = monitor interval (default 1)
    # :g_addr = gateway address (default 10.0.0.1)
    # :log    = log name (default pings.txt)
    # :max_crapouts = maximum size of the crapout array (default 100)
    def initialize(options = {})
      Thread.abort_on_exception = true; @threads = {}; @crapouts = []
      @opt = Thread.current[:options] = options
      set_default_options()
      
      start_gateway() unless @opt[:no_gateway]
      start_ping()    unless @opt[:no_ping]
      start_monitor() unless @opt[:no_monitor]
    end
    
    def report
      
    end
    
    # Return all data or block result.
    def data(&filter)
      filter ||= lambda {|d| d.split("\n").map {|l| l.split(',').map {|f| f.strip}}}
      data = Thread.exclusive {File.open(@opt[:log], 'r') {|f| f.read}}
      filter.call(data)
    end
    
    # Returns the current interval of the given thread.
    def get_interval(which = :p_int)
      Thread.exclusive {@opt[which]}
    end
    
    # Sets a new interval of the given thread.
    def set_interval(which = :p_int, interval = DEF_PING)
      Thread.exclusive {@opt[which] = interval if @opt.has_key(which)}
    end
    
    # Return a *copy* of the crapout array.
    def crapouts
      Thread.exclusive {@crapouts.dup}
    end
    
    # Add a ping target. Do some validation here huh? almost 39 on the don juan
    def add_target(target)
      @@targets << target
    end
    
    private
    
    # Set default options. Options are in a hash stored in a thread var,
    # _t[:options]_. At this point, there is no multithreading going on so
    # locking is not necessary.
    def set_default_options
      @opt[:g_int]  ||= DEF_GWP
      @opt[:p_int]  ||= DEF_PING
      @opt[:m_int]  ||= DEF_MON
      @opt[:g_addr] ||= DEF_GWA #should be looking for gateway.
      @opt[:log]    ||= DEF_LOG
      @opt[:max_crapouts] ||= DEF_MAX_CRAPOUTS
    end
    
    # Start the gateway check thread. Monitors gateway (because trying to
    # ping the world when the gateway is down is fucking stupid).
    def start_gateway
      @threads[:gateway] = Thread.new(Thread.current) do |notify|
        opt, gwa = Thread.exclusive {[notify[:options], notify[:options][:g_addr]]}
        loop do
          sleep(Thread.exclusive {opt[:g_int]})
          if `ping #{gwa}`.index('timed')
            Thread.exclusive {notify[:idle] = true}
            next
          else
            Thread.exclusive {notify[:idle] = false}
          end
        end
      end
    end
    
    # Start the ping thread. Pings random targets all over the world. Note that
    # their ICMP reply status is assumed. That is, do not add a target unless it
    # replies to an ICMP echo request.
    def start_ping
      @threads[:ping] = Thread.new(Thread.current) do |notify|
        opt = Thread.exclusive {notify[:options]}
        loop do
          sleep(Thread.exclusive {opt[:p_int]})
          next if Thread.exclusive {notify[:idle]}
          targ = @@targets[rand * @@targets.count]
          r = "[#{targ}]" + `ping #{targ} /n 1`.split("\r\n")[3] rescue "[#{targ}]"
          Thread.exclusive {notify[:reply] = r}
        end
      end
    end
    
    # Start the monitor thread. The monitor is responsible for reading the ping
    # replies and archiving them. It also keeps a (limited) log of timeouts and
    # latencies in memory as the array .crapouts. This is a list of ping records
    # with latencies over 500ms and timeouts, be they gateway or network.
    def start_monitor
      @threads[:monitor] = Thread.new(Thread.current) do |notify|
        opt = Thread.exclusive {notify[:options]}
        f = File.new(Thread.exlusive {opt[:log]}, 'a')
        begin
          gw = last = nil
          loop do
            sleep(Thread.exclusive {opt[:m_int]})
            next if Thread.exclusive {last == notify[:reply]}
            gw, last = Thread.exclusive {[notify[:idle], notify[:reply]]}
            last =~ /\[(.+)\].+(time=(\d{1,4})ms|timed)/
            now = Time.now; site = $1; time = $3.to_i
            as_str = "#{now}, #{gw ? 'gateway is down' : time}, #{site}"
            f.puts(as_str); f.flush
            Thread.exclusive do
              @crapouts << as_str if 500 <= time or 0 == time
              @crapouts = @crapouts[1...-1] if @crapouts.count > opt[:max_crapouts]
            end
          end
        ensure
          f.close
        end
      end
        
    end
    
  end #cls Net
  
end #mod Metrics
