#!/usr/bin/env ruby
# ex: ft=ruby

require "yaml"
require "io/console"
load "dvmtools.rb"

# Everything we do has to fit in above STATUS_ROW
# At some point I should make this dynamic...
STATUS_ROW = 5
CSI = "\e["
init
interrupted = false
interval_remaining = 1
while true do
  # reload the config for each sync so that we pick up changes
  # TODO - merge in custom settings...
  config = load_settings['vm']['sync']
  # TODO how to safely stop any interrupt from passing through to child process
  # before we intercept?
  begin
    interval_remaining = interval_remaining - 1
    if interval_remaining == 0
      r = do_sync(config)
      interval_remaining = config['interval'] + 1
      # Prevent scrolling and resizing from doing ugly things...
      @screen.clear
      @screen.say_at(1, 1, banner)
      output = parse_results(r)
      interrupted = false
      refresh_stats(output)
    else
      refresh_sync(config, "#{interval_remaining} seconds")
      sleep(1)
    end

  rescue Interrupt => e
    @screen.clear
    if interrupted
      quit
    else
      interrupted = true
      @screen.say_at 1, 1, pause_message
      result = @screen.next_char
      case result
      when ?q, "\u0003"
        quit
      else
        @screen.show_banner
        interrupted = false
      end
    end
  rescue Exception => e
    @screen.clear
    @screen.say_at 1,1, "Oops, something went wrong. Here's the info: "
    @screen.move_to 3,1
    raise
  end
end

# Because I want my functions at the bottom of a script, damnit.
BEGIN {
  def init
    @num_syncs = 0
    @screen = Screen.new
    @screen.show_banner
    refresh_stats(transferred: 0, walltime: 0)
    refresh_sync({"show-syncing-message" => true} , "Soon.")
    @screen.set_status 'Loading SSH config...'
    @all_stats = { walltime: 0.0,  transferred: 0, created: 0, deleted: 0, bytes: 0, time: 0.0}
    # Determine our ssh args up front, so we won't need to do this on every sync.
    ssh_info = {}
    vagrant_cmd_string = ENV['VAGRANT_PATH'].nil? ? 'vagrant' : ENV['VAGRANT_PATH']
    `#{vagrant_cmd_string} ssh-config chef-server`.split("\n").each do |line|
      k, v = line.split(" ")
      ssh_info[k.strip] = v.strip
    end
    @screen.clear_status
    @username = ssh_info["User"]
    @host = ssh_info["HostName"]
    @rsh = "ssh -p #{ssh_info["Port"]} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -q -i #{ssh_info["IdentityFile"]}"
  end

  def refresh_sync(config, message)
    return unless config['show-syncing-message']
    @screen.say_at(4, 10, "#{@screen.colortext(:purple, "Next Sync", true)}: #{@screen.colortext(:blue, message)}")
  end
  def do_sync(config)

    # use  minimal text change to avoid drawing the eye to movement on the screen
    # if someone doens't want to be actively looking...
    refresh_sync(config, "0 seconds...")
    start = Time.now
    excludes = Array(config['exclude-files']).map(&:to_s)
    excludes += ['.vagrant/']
    excludes.uniq!
    args = ["--stats", "--archive", "--delete", "-z",
            "--no-owner", "--no-group", "--rsync-path /usr/bin/rsync", "-k" ]
    command = [ "rsync", args, "-e", "\"#{@rsh}\"", excludes.map { |e| ["--exclude", e] },
                "../", "#{@username}@#{@host}:/host" ].flatten.join(" ")
    # TODO - subproc to ignore INT signal, or move to different process group
    # to avoid Ctrl+C interrupting it.
    r = `#{command}`
    [ Time.now - start, r.split("\n") ]
  end

  # Honestly, does anyone care?
  def refresh_stats(output)
    @screen.clear_status
    color = @screen.color(:purple, true)
    ec = @screen.endcolor
    @screen.say_at(3, 2,  "#{color}Files Transferred:#{ec} #{output[:transferred]}                ")
    @num_syncs += 1
  end

  def quit
    @screen.say_at 4, 1, "File sync is now terminated."
    @screen.say_at 5, 1, "The VM will remain running."
    @screen.say_at 6, 1, "To resume syncing, just run:"
    @screen.say_at 7, 4, "#{@screen.colortext(:purple, "./sync")}"
    exit 0
  end



  def parse_results(results)
    walltime, results = results
    stats = {}
    stats[:time] = 0.0
    stats[:walltime] = walltime
    results.each do |line|
      case line
      when /Number of created files: (.*)/ # rsync 3 on linux
        stats[:created] = $1.gsub(",", "").to_i
      when /Number of deleted files: (.*)/ # rsync 3 on linux
        stats[:deleted] = $1.gsub(",", "").to_i
      when /Number of .* transferred: (.*)/ # rsync 3 on linux (number of files) and rsync 2 on mac (number of regular files)
        stats[:transferred] = $1.gsub(",", "").to_i
      when /.* time: (.*) / # rsync 3 on linux and rsync 2 on mac. Two lines indicating time expected.
        stats[:time] += $1.gsub(",", "").to_f
      when /Total transferred file size: (.*) bytes/ # rsync 3 on linux and rsync 2 on mac
        stats[:bytes] = $1.gsub(",", "").to_i
      end
    end
    stats.each do |k, v|
      @all_stats[k] = @all_stats[k] + v
    end
    stats
  end

  class Screen
    attr_reader :rows, :cols
    def initialize()
      @rows, @cols = $stdin.winsize
      @colors =  { black: 0, red: 1, green: 2, brown: 3, blue: 4, purple: 5, cyan: 6, grey: 7 }
    end

    def clear_status
      set_status("")
    end
    def show_banner
      clear
      say_at(1, 1, banner)
    end


    def set_status(message)
      # Refresh in case of resize
      @rows, @cols = $stdin.winsize
      # Auto-fill to end of line with spaces. Trust that the message length
      # isn't longer than the # cols...
      # TODO Might be an escape code we can use to clear to end of line
      message = "#{message}#{" " * (cols - message.length)}"
      save_pos
      say_at([rows, STATUS_ROW].min, 1, message)
      restore_pos
    end

    def endcolor
      "#{CSI}0m"
    end
    def colortext(c, text, bright = false)
      "#{color(c, bright)}#{text}#{endcolor}"
    end
    def color(color, bright = false)
      "#{CSI}#{30+@colors[color]}#{bright ? ';1' : ';0'}m"
    end
    def bgcolor(color, bright = false)
      "#{CSI}#{40+@colors[color]}#{bright ? ';1' : ';0'}m"
    end
    def say_at(row, col, message)
      move_to(row, col)
      $stdout.write(message)
    end

    def move_to(row, col); csi "#{row};#{col}H"; end
    def clear; csi "2J";  end
    def save_pos;  csi "s"; end
    def restore_pos; csi "u"; end
    def csi(out)
      STDOUT.write "#{CSI}#{out}"
    end

    def next_char
      $stdin.getch
    end
  end

  def pause_message
<<-EOM
Syncing paused. Type 'q' or Ctrl+C
again to stop sync. Type any other
key to continue.
EOM
  end
  def banner
<<-EOM
       Ctrl+C to pause sync.

EOM
  end
} # BEGIN



# 17117 files to consider
#

# TODO a couple of things to explore
# - rsync daemon - faster? Doesit matter, since we're already well under a second
#   for reasonable syncs?
# - can we flip this around and just have rsync in the guest pull from the host
#   at cron'd intervals? That avoids having to leave something running.
#  (to get to host ip, ifconfig en0 | grep "inet " | cut -d" " -f2)
