#!/usr/bin/env ruby

=begin

  =Description
    keyspeedapplet - an applet for the system tray 
                     to show the speed, you are typing

  =Depends
         * ruby
         * libgtk2-ruby
         * modified xspy

  =Author
    written by Benjamin Kellermann <Benjamin dot Kellermann at gmx in Germany>

  =Licence
    this code is licenced under GPL Version 2

=end

CONFIGFILE = File.expand_path("~/.keyspeedrc.yaml")
UPDATESEC = 15
TRAY_COLOR = "#B93208"
TRAY_X = 20
TRAY_Y = 23

begin
  require 'gtk2'
rescue LoadError
  $stderr << "########################################################\n"
  $stderr << "# In order to run this programm you need ruby-gnome2!  #\n"
  $stderr << "########################################################\n"
  raise
end

require 'yaml'
require 'pp'
require 'optparse'
require 'ostruct'

KEYSPEEDAPPLETVERSION = [1,0]




class Time
  def roundto_day
    Time.local(self.year, self.month, self.day)
  end
end

class NilClass
  def succ
    1
  end
end

class Hash
  def inc!(value)
    self[value] = self[value].succ    
  end
end

class Score
  attr_accessor :time, :kpm, :errors
  def initialize(time, kpm, errors)
    @time = time
    @kpm = kpm
    @errors = errors
  end
  
  include Comparable
  def <=> (other)
    if self.kpm == other.kpm
      return other.errors <=> self.errors
    else
      return self.kpm <=> other.kpm
    end
  end
  def to_s
    "#{@kpm} kpm at #{@time.strftime('%d.%m.%Y')} with #{@errors.to_s.rjust(2)} errors"
  end
  def inspect
    "[Score:@time: #{@time.inspect} @kpm: #{@kpm.inspect} @errors: #{@errors.inspect}]"
  end
end

class HighScore < Array
  def initialize(number)
    number.times do |t| 
      a = Score.new(Time.new.roundto_day - 60*60*24*t,100,0)
      self.push(a)
    end
  end
  def to_s 
    ret = ""
    a = self.size.to_s.length
    self.each_index do |i|
      ret += "#{(i+1).to_s.rjust(a)}. #{self[i]}\n"
    end
    ret
  end
  
  # returns nil if day is not in highscore list
  def get_day(day)
    self.each { |e| return e if e.time == day }
    nil
  end
  
  # returns true, if a new score was reached
  def check_top!(score)
    # check if it got it into the highscore
    if score > self.last
      # check if the day is already in the highscore
      todayscore = self.get_day(score.time.roundto_day)
      if todayscore 
        if todayscore > score
          return false
        else
          todayscore.kpm = score.kpm
          todayscore.errors = score.errors
        end
      else
        self[-1].time = score.time.roundto_day
        self[-1].kpm = score.kpm
        self[-1].errors =  score.errors
      end
      self.sort!.reverse!
      return true
    end  
    false
  end
end

class ConfigFile
   attr_accessor :maxkpm, :maxpeak, :wrongcount
  def initialize
    @maxkpm ||= HighScore.new(10)
    @maxpeak ||= HighScore.new(3)
    @wrongcount ||= Hash.new
  end
  alias init initialize
  public :init
  def highscores_to_s
    ret = "Top #{self.maxpeak.size} peak speed:\n"
    ret += self.maxpeak.to_s
    ret += "Top #{self.maxkpm.size} speed list:\n"
    ret += self.maxkpm.to_s
    ret
  end
end

class KeyFifo < Array
  def update!(seconds)
    while  (self[0] && self[0][0] < Time.now - seconds)
      self.shift
    end
  end
  
  def peak(seconds)
    fifosize = self.size
    keys = 0
    while  (keys < fifosize && self[fifosize-keys-1][0] >= Time.now - seconds)
      keys += 1
    end
    keys*60/seconds
  end
end

class Keyapplet
  def initialize
    ############################################################################
    # init the things from the CONFIGFILE
    ############################################################################
    unless File.exist?(CONFIGFILE) and @config = YAML::load_file(CONFIGFILE)
      @config = ConfigFile.new
      writeconfig
      File.chmod(0600, CONFIGFILE)
    end
    @config.init

    @minutefifo = KeyFifo.new
    @errorfifo = KeyFifo.new

    @session = Score.new(Time.new,0,0)
    @peak = Score.new(Time.new,1,99999)


    ############################################################################
    # init the tray and GTK stuff
    ############################################################################
    @sicon = Gtk::StatusIcon.new

		@speedhist = []
		(TRAY_X-4).times{ @speedhist << 1.0 }

    # Add a menu
    menu = Gtk::Menu.new


    speed = Gtk::MenuItem.new("Show current Speed")
    speed.signal_connect("activate"){	@speedwin.show_all }
    menu.append(speed.show)

    scores = Gtk::MenuItem.new("Show Highscores")
    scores.signal_connect("activate") do
			@highscoreslabel.text = "Today:\nPeak: #{@peak.kpm} kpm " + 
			  @peak.time.strftime("at %H:%M:%S") +
				" with #{@peak.errors} errors\n" +
				"1 Min: #{@session.kpm} kpm " +
			  @peak.time.strftime("at %H:%M") +
			  " with #{@session.errors} errors\n" +
			  @config.highscores_to_s

				@highscores.show_all
		end
    menu.append(scores.show)

    @sicon.signal_connect("activate") do
      @sicon.blinking = false
    end

    
    refresh = Gtk::MenuItem.new("Reload Highscorefile")
    refresh.signal_connect("activate") do
      @config = YAML::load_file(CONFIGFILE) 
    end

    menu.append(refresh.show)

    ############################################################################
    # set the about dialog
    ############################################################################
    aboutitem = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT)
    aboutitem.signal_connect("activate") {
          Gtk::AboutDialog.set_email_hook {|about, link|
        `gnome-open mailto:#{link}`
      }
      Gtk::AboutDialog.set_url_hook {|about, link|
        `gnome-open #{link}`
      }

      a = Gtk::AboutDialog.new
      a.authors = ["Benjamin Kellermann <Benjamin.Kellermann@gmx.de>"]
      a.comments  = "This is an applet to show the speed you are typing."
      a.copyright = "Copyright (C) 2006 Benjamin Kellermann"
      a.license   = "This program is licenced under GPLv2"
      a.logo_icon_name      = "gtk-about"
      a.name      = "keyspeedapplet"
      a.version   = KEYSPEEDAPPLETVERSION.join(".")
      a.website   = "http://www.eigenheimstrasse.de/~ben/keyspeedapplet/"
      a.website_label = "Download Website"
      a.signal_connect('response') { a.destroy }
      a.show 
    }
    menu.append(aboutitem.show)

    # add an exit button
    quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
    quit.signal_connect("activate") { Gtk.main_quit }
    menu.append(Gtk::SeparatorMenuItem.new.show)
    menu.append(quit.show)

    @highscores = Gtk::Window.new("Highscores")
    @highscores.resizable = false
    @highscoreslabel = Gtk::Label.new
    @highscores.add(@highscoreslabel)
    @highscores.signal_connect("delete-event") { 
      @highscores.hide
    }
    
		@speedwin = Gtk::Window.new("Current Speed")
    @speedwin.resizable = false
    @speedlabel = Gtk::Label.new
    @speedwin.add(@speedlabel)
    @speedwin.signal_connect("delete-event") { 
      @speedwin.hide
    }

		@sicon.signal_connect('popup-menu') do |w,e,time|
      menu.popup(nil, nil, e, time) 
	  end
  end
  

  def writeconfig
    File.open(CONFIGFILE, 'w') do |out|
      out << "# This is the state file for keyspeedapplet\n"
      out << @config.to_yaml
    end
  end

  def add_key(key)
    if key =~ /BackSpace/
      if (@minutefifo.last != nil && @minutefifo.last[0] > Time.now - 2)
        @errorfifo << [Time.now, "Backspace"]
        if $options.wrongcount
          @config.wrongcount.inc!(@minutefifo.pop[1])
          writeconfig
        end
      end
    end
    @minutefifo << [Time.now, key.chomp]
  end

  def updatetray

    @minutefifo.update!(60)
    @errorfifo.update!(60)
    
    curscore = Score.new(Time.now,@minutefifo.size,@errorfifo.size)
    curpeak = Score.new(Time.now, @minutefifo.peak(UPDATESEC), @errorfifo.peak(UPDATESEC))
    
    if @session < curscore
      @session = curscore
      if @config.maxkpm.check_top!(@session)
        writeconfig
        @sicon.blinking = true
      end
    end
    if @peak < curpeak
      @peak = curpeak
      if @config.maxpeak.check_top!(@peak)
        writeconfig
        @sicon.blinking = true
      end
    end
		
    # update_tooltip
		text = "Peak: #{curpeak.kpm} kpm with #{curpeak.errors} errors, Min: #{curscore.kpm} kpm with #{curscore.errors} errors" 
		@sicon.set_tooltip(text)
		@speedlabel.text = text

		@speedhist.shift
		@speedhist << curpeak.kpm.to_f / @config.maxpeak[0].kpm


		pic = [ "#{TRAY_X} #{TRAY_Y} 5 1",
		".	c #{TRAY_COLOR}",
		"a	c #A5A2A5",
		"o	c #DEDBDE",
		"u	c #F7F3F7",
		" 	c #000000"]


		pic << "a"*TRAY_X
		pic << "a" + "o"*(TRAY_X-3) + "ua"

		inner = @speedhist.map do |speed|
			bar = ((TRAY_Y-4)*speed).to_i
			["."]*bar + [" "]*(TRAY_Y-4-bar)
		end
		pic += inner.transpose.reverse.map{ |line| "ao#{line.join("")}ua" }
		
		pic << "a" + "u"*(TRAY_X-2) + "a"
		pic << "a"*TRAY_X
		@sicon.pixbuf = Gdk::Pixbuf.new(pic)
  end
end

########
# main #
########
if __FILE__ == $0

  Gtk.init
  $options = OpenStruct.new
    
  optpars = OptionParser.new do |opts|
      
    opts.banner = "Usage: #{File.basename($0)} [options]"
    
    opts.separator "Options:"
    
    opts.on("--enable-wrongcount", "Do not make a statistic over wrong typed letters.") do |bool|
      $options.wrongcount = bool
    end

    opts.on_tail("--version", "Show version and exit") do
      puts "keyspeedapplet #{KEYSPEEDAPPLETVERSION.join('.')}"
      puts "written by Benjamin Kellermann <Benjamin.Kellermann@gmx.de>"
      exit
    end
  end

  begin
    optpars.parse!
  rescue => e
    puts e
    puts optpars
    exit
  end

  begin
    mytray = Keyapplet.new
    Thread.main.priority = -20
    
    xspy = IO.popen("xspy")

    Thread.new {
      xspy.each_line { |line| 
        mytray.add_key line
      }
    }
    Gtk.timeout_add(900){ mytray.updatetray; true }
    Gtk.main
  rescue
    puts "Maybe you upgraded to a newer Version and the syntax of the configfile changed?"
    puts "Remove #{CONFIGFILE} to get a new plain one!"
    raise
  end
end
