#!/usr/bin/ruby -w

# copyright Tom Gilbert, 2002
# see COPYING for license information

require 'gtk'
require 'XTest'

class State
  MODE_SCROLL = 1
  MODE_LIST = 2
  MODE_VOLUME = 3
  MODE_BRIGHTNESS = 4
  @@mode = 1
  def State.mode
    @@mode
  end
  def State.mode=(mode)
    @@mode = mode
  end
  def State.MODE_SCROLL
    return MODE_SCROLL
  end
  def State.MODE_LIST
    return MODE_LIST
  end
  def State.MODE_VOLUME
    return MODE_VOLUME
  end
  def State.MODE_BRIGHTNESS
    return MODE_BRIGHTNESS
  end
end

class RSJog < Gtk::Window

  SONYPI_EVENT_JOGDIAL_DOWN = 1
  SONYPI_EVENT_JOGDIAL_UP = 2
  SONYPI_EVENT_JOGDIAL_DOWN_PRESSED = 3
  SONYPI_EVENT_JOGDIAL_UP_PRESSED = 4
  SONYPI_EVENT_JOGDIAL_PRESSED = 5
  SONYPI_EVENT_JOGDIAL_RELEASED = 6
  SONYPI_EVENT_CAPTURE_PRESSED = 7
  SONYPI_EVENT_CAPTURE_RELEASED = 8
  SONYPI_EVENT_CAPTURE_PARTIALPRESSED = 9
  SONYPI_EVENT_CAPTURE_PARTIALRELEASED = 10
  SONYPI_EVENT_FNKEY_ESC = 11
  SONYPI_EVENT_FNKEY_F1 = 12
  SONYPI_EVENT_FNKEY_F2 = 13
  SONYPI_EVENT_FNKEY_F3 = 14
  SONYPI_EVENT_FNKEY_F4 = 15
  SONYPI_EVENT_FNKEY_F5 = 16
  SONYPI_EVENT_FNKEY_F6 = 17
  SONYPI_EVENT_FNKEY_F7 = 18
  SONYPI_EVENT_FNKEY_F8 = 19
  SONYPI_EVENT_FNKEY_F9 = 20
  SONYPI_EVENT_FNKEY_F10 = 21
  SONYPI_EVENT_FNKEY_F11 = 22
  SONYPI_EVENT_FNKEY_F12 = 23
  SONYPI_EVENT_FNKEY_1 = 24
  SONYPI_EVENT_FNKEY_2 = 25
  SONYPI_EVENT_FNKEY_D = 26
  SONYPI_EVENT_FNKEY_E = 27
  SONYPI_EVENT_FNKEY_F = 28
  SONYPI_EVENT_FNKEY_S = 29
  SONYPI_EVENT_FNKEY_B = 30
  SONYPI_EVENT_BLUETOOTH_PRESSED = 31
  SONYPI_EVENT_BACK = 35

  def initialize()
    super(Gtk::WINDOW_TOPLEVEL)
    set_title("rsjog")
    border_width = 0
    set_position(Gtk::WIN_POS_CENTER)
    signal_connect("delete_event") { delete }
    signal_connect("destroy") { destroy }

    @xtest = XTest.new
    @bindings = Array.new
    
    @clist = create_clist()
    add(@clist)

    load_config()
    trap("HUP") { load_config() }

    @timer = nil
    State.mode = State.MODE_SCROLL
    
    @vol = VolScroller.new("rsjog: volume", @volume_xpm, 
                            0, 100, @volume_step)
    @bright = BrightScroller.new("rsjog: brightness", @brightness_xpm,
                            0, 255, @brightness_step)
    @bright.bar.set_format_string("%p%% (%v)")
    
    @listener = SonyPIListener.new("/dev/sonypi")
    @listener.listen { |event| wheelevent(event) }

    Gtk::main()
  end

  alias oldshow show
  def show
    check_rc()
    check_actions()
    oldshow()
  end

  alias oldhide hide
  def hide
    stop_timer()
    oldhide()
  end
  
  def action(action)
    return if action == nil
    if (action =~ /^\*(.*)/)
      action = $1
      if (action == "volume")
        @bright.hide()
        State.mode = State.MODE_VOLUME
        @vol.start_timer()
        @vol.show()
      elsif (action == "brightness")
        @vol.hide()
        State.mode = State.MODE_BRIGHTNESS
        @bright.start_timer()
        @bright.show()
      elsif (action =~ /^button(\d)$/)
        button = $1.to_i
        State.mode = State.MODE_SCROLL
        hide()
        @xtest.button_press(button)
      elsif (action == "cancel")
        State.mode = State.MODE_SCROLL
        hide()
      elsif (action == "quit")
        exit(0)
      end
    else
      system "#{action}&"
    end
  end

  def create_clist
    clist = Gtk::CList::new(["Name"])
    clist.set_column_auto_resize(0, true)
    clist.set_selection_mode(Gtk::SELECTION_BROWSE)
    clist.set_column_justification(0, Gtk::JUSTIFY_LEFT)
    clist.column_titles_hide
    clist.show()
    return clist
  end

  def load_rc

    # default settings
    @rc_file = ENV["HOME"] + "/.rsjogrc"
    @actions_file = ENV["HOME"] + "/.rsjog.menu"
    @volume_xpm = "/usr/local/share/rsjog/volume.xpm"
    @brightness_xpm = "/usr/local/share/rsjog/brightness.xpm"
    @hide_timeout = 3000
    @brightness_step = 25
    @volume_step = 10
    @rc_check = true
    
    if !FileTest.file?(@rc_file)
      @rc_mtime = 0
      return
    end
    @rc_mtime = File.stat(@rc_file).mtime
    IO.readlines(@rc_file).each {|line|
      line.chomp!
      next if line =~ /^\s*#/
      next if line =~ /^\s*$/
      case line
      when (/^\s*actions file\s*=\s*(.+)$/i)
        @actions_file = $1
      when (/^\s*brightness xpm\s*=\s*(.+)$/i)
        @brightness_xpm = $1
      when (/^\s*volume xpm\s*=\s*(.+)$/i)
        @volume_xpm = $1
      when (/^\s*bind\s+(\S+)\s+(.+)$/i)
        bind_event $1, $2
      when (/^\s*volume step\s*=\s*(.+)$/i)
        @volume_step = $1.to_i
      when (/^\s*brightness step\s*=\s*(.+)$/i)
        @brightness_step = $1.to_i
      when (/^\s*hide timeout\s*=\s*(.+)$/i)
        @hide_timeout = $1.to_i * 1000
      when (/^\s*auto rc check = no\s*$/i)
        @rc_check = false
      else
        puts "rsjog: unrecognised line '#{line}' in rc file #@rc_file"
      end
    }
  end

  def check_rc
    return unless @rc_check
    load_rc() if ((!FileTest.file?(@actions_file)) || File.stat(@rc_file).mtime > @rc_mtime)
  end
  
  def load_actions
    @actions = Array.new
    if !FileTest.file?(@actions_file)
      @actions_mtime = 0
    else
      @actions_mtime = File.stat(@actions_file).mtime
      IO.readlines(@actions_file).each {|line|
        line.chomp!
        next if line =~ /^\s*#/
        next if line =~ /^\s*$/
        @actions << Action.new(line)
      }
    end
    @clist.freeze()
    @clist.clear()
    @actions.each { |i|
      @clist.append([i.label])
    }
    @select_row = 0
    @clist.select_row(@select_row, 0)
    @clist.thaw()
  end

  def check_actions
    return unless @rc_check
    load_actions() if ((!FileTest.file?(@actions_file)) || File.stat(@actions_file).mtime > @actions_mtime)
  end

  def load_config
    load_rc()
    load_actions()
  end

  def bind_event(event, action)
    case event
      when "CAPTURE_PRESSED"
        @bindings[SONYPI_EVENT_CAPTURE_PRESSED] = action
      when "CAPTURE_RELEASED"
        @bindings[SONYPI_EVENT_CAPTURE_RELEASED] = action
      when "CAPTURE_PARTIALPRESSED"
        @bindings[SONYPI_EVENT_CAPTURE_PARTIALPRESSED] = action
      when "CAPTURE_PARTIALRELEASED"
        @bindings[SONYPI_EVENT_CAPTURE_PARTIALRELEASED] = action
      when "FNKEY_ESC"
        @bindings[SONYPI_EVENT_FNKEY_ESC] = action
      when "FNKEY_F1"
        @bindings[SONYPI_EVENT_FNKEY_F1] = action
      when "FNKEY_F2"
        @bindings[SONYPI_EVENT_FNKEY_F2] = action
      when "FNKEY_F3"
        @bindings[SONYPI_EVENT_FNKEY_F3] = action
      when "FNKEY_F4"
        @bindings[SONYPI_EVENT_FNKEY_F4] = action
      when "FNKEY_F5"
        @bindings[SONYPI_EVENT_FNKEY_F5] = action
      when "FNKEY_F6"
        @bindings[SONYPI_EVENT_FNKEY_F6] = action
      when "FNKEY_F7"
        @bindings[SONYPI_EVENT_FNKEY_F7] = action
      when "FNKEY_F8"
        @bindings[SONYPI_EVENT_FNKEY_F8] = action
      when "FNKEY_F9"
        @bindings[SONYPI_EVENT_FNKEY_F9] = action
      when "FNKEY_F10"
        @bindings[SONYPI_EVENT_FNKEY_F10] = action
      when "FNKEY_F11"
        @bindings[SONYPI_EVENT_FNKEY_F11] = action
      when "FNKEY_F12"
        @bindings[SONYPI_EVENT_FNKEY_F12] = action
      when "FNKEY_1"
        @bindings[SONYPI_EVENT_FNKEY_1] = action
      when "FNKEY_2"
        @bindings[SONYPI_EVENT_FNKEY_2] = action
      when "FNKEY_D"
        @bindings[SONYPI_EVENT_FNKEY_D] = action
      when "FNKEY_E"
        @bindings[SONYPI_EVENT_FNKEY_E] = action
      when "FNKEY_F"
        @bindings[SONYPI_EVENT_FNKEY_F] = action
      when "FNKEY_S"
        @bindings[SONYPI_EVENT_FNKEY_S] = action
      when "FNKEY_B"
        @bindings[SONYPI_EVENT_FNKEY_B] = action
      when "BLUETOOTH_PRESSED"
        @bindings[SONYPI_EVENT_BLUETOOTH_PRESSED] = action
      else
        # support events we don't know about
        @bindings[event.to_i] = action
    end
  end

  def clist_select_next
    if (@select_row == @clist.rows - 1)
      @select_row = 0;
    else
      @select_row += 1;
    end
    @clist.select_row(@select_row,0)
  end

  def clist_select_prev
    if (@select_row == 0)
      @select_row = @clist.rows - 1
    else
      @select_row -= 1
    end
    @clist.select_row(@select_row,0)
  end

  def start_timer
    stop_timer()
    @timer = Gtk::timeout_add(@hide_timeout) { timeout() }
  end

  def timeout
    State.mode = State.MODE_SCROLL
    hide()
  end

  def stop_timer
    return if @timer == nil
    Gtk::timeout_remove(@timer)
    @timer = nil
  end

  def jog_down
    if (State.mode == State.MODE_LIST)
      clist_select_next()
      start_timer()
    elsif (State.mode == State.MODE_VOLUME)
      @vol.start_timer()
      @vol.dec()
    elsif (State.mode == State.MODE_BRIGHTNESS)
      @bright.start_timer()
      @bright.dec()
    else
      # button 5!
      @xtest.button_press(5)
    end
  end

  def jog_up
    if (State.mode == State.MODE_LIST)
      clist_select_prev()
      start_timer()
    elsif (State.mode == State.MODE_VOLUME)
      @vol.start_timer()
      @vol.inc()
    elsif (State.mode == State.MODE_BRIGHTNESS)
      @bright.start_timer()
      @bright.inc()
    else
      # button 4!
      @xtest.button_press(4)
    end
  end

  # TODO
  # replace this with a state machine
  def wheelevent(event)
    case event
      when SONYPI_EVENT_JOGDIAL_DOWN
        jog_down()
      when SONYPI_EVENT_JOGDIAL_DOWN_PRESSED
        jog_down()
      when SONYPI_EVENT_JOGDIAL_UP
        jog_up()
      when SONYPI_EVENT_JOGDIAL_UP_PRESSED
        jog_up()
      when SONYPI_EVENT_JOGDIAL_PRESSED
        if State.mode == State.MODE_SCROLL
          if (@actions.length > 0)
            State.mode = State.MODE_LIST
            show()
          else
            @xtest.button_press(2)
          end
        elsif State.mode == State.MODE_VOLUME
          State.mode = State.MODE_SCROLL
          @vol.hide()
        elsif State.mode == State.MODE_BRIGHTNESS
          State.mode = State.MODE_SCROLL
          @bright.hide()
        else
          State.mode = State.MODE_SCROLL
          hide()
          @clist.each_selection {|row|
            action(@actions[row].action)
            break
          }
        end
      when SONYPI_EVENT_JOGDIAL_RELEASED
        if (State.mode == State.MODE_LIST)
          start_timer()
        end
      when SONYPI_EVENT_BACK
        if (State.mode == State.MODE_LIST)
          State.mode = State.MODE_SCROLL
          hide()
        elsif (State.mode == State.MODE_VOLUME)
          State.mode = State.MODE_LIST
          @vol.hide()
          show()
        elsif (State.mode == State.MODE_BRIGHTNESS)
          State.mode = State.MODE_LIST
          @bright.hide()
          show()
        else
          # button 3!
          @xtest.button_press(2)
        end
      else
        # user bound events
        action(@bindings[event])
    end
  end
  
  def delete
    hide()
    true
  end

  def destroy
    exit
  end

end

class SonyPIListener
  def initialize(devpath)
    @devpath = devpath
  end
  def listen(&block)
    @dev = File.new(@devpath, "r")
    @block = block
    Gtk::input_add(@dev, Gdk::INPUT_READ) {read_ev}
  end
  def read_ev
    event = @dev.getc
    @block.call event
  end
  def close
    @dev.close
  end
end

class Action
  attr_reader :label
  attr_reader :action
  def initialize(line)
    if (line =~ /^\s*"(.*?)"\s+(.*)$/)
      @label = $1
      @action = $2
    elsif (line =~ /^\s*(\S+)\s+(.*)$/)
      @label = $1
      @action = $2
    else
      @label = "error"
      @action = "error"
    end
  end
end

class ScrollWindow < Gtk::Window
  attr_reader :bar
  def initialize(title, pixmap, min, max, step)
    super(Gtk::WINDOW_TOPLEVEL)
    set_title(title)
    set_position(Gtk::WIN_POS_CENTER)
    signal_connect("delete_event") { delete }
    signal_connect("destroy") { destroy }
    
    @timer = nil
    @hide_timeout = 3000

    vbox = Gtk::VBox::new
    vbox.border_width = 10
    add(vbox)
    realize()
    pm, mask = Gdk::Pixmap::create_from_xpm(window, nil, pixmap)
    gpm = Gtk::Pixmap::new(pm, mask)
    gpm.show()
    vbox.add(gpm)
    @adj = Gtk::Adjustment::new(initval, min, max, step, 2 * step, 1)
    @bar = Gtk::ProgressBar::new(@adj)
    @bar.set_format_string("%p%%")
    @bar.set_show_text(true)
    @bar.show()
    vbox.add(@bar)
    vbox.show()
  end

  alias oldhide hide
  def hide
    stop_timer()
    oldhide()
  end

  alias oldshow show
  def show
    @bar.set_value initval()
    oldshow()
  end

  def value=(value)
    @bar.set_value value
  end

  def inc
    adj = @bar.adjustment
    new_val = adj.value + adj.step_increment
    new_val = adj.upper if new_val > adj.upper
    if (adj.value != new_val)
      @adj.value = new_val
      setval(new_val)
    end
  end

  def dec
    adj = @bar.adjustment
    new_val = adj.value - adj.step_increment
    new_val = adj.lower if new_val < adj.lower
    if (adj.value != new_val)
      @adj.value = new_val
      setval(new_val)
    end
  end

  def start_timer
    stop_timer()
    @timer = Gtk::timeout_add(@hide_timeout) { timeout() }
  end

  def timeout
    State.mode = State.MODE_SCROLL
    hide()
  end

  def stop_timer
    return if @timer == nil
    Gtk::timeout_remove(@timer)
    @timer = nil
  end

  def delete
    stop_timer()
    hide()
    true
  end

  def destroy
    exit
  end
  def setval(value)
  end
  def initval
  end
end

class VolScroller < ScrollWindow
  def initval
    vol = `aumix -vq`
    vol.chomp!
    if (vol =~ /^vol\s*(\d+)/)
      val = $1.to_i
      # round up to a factor of the step
      # return val + (@volume_step - (val % @volume_step))
      return val
    else
      return 0
    end
  end

  def setval(value)
    system("aumix -v#{value.to_i}")
  end
end

class BrightScroller < ScrollWindow
  def initval()
    bright = `spicctrl -B`
    return bright.to_i
    #b = bright.to_f
    #val = (b * 100 / 255).to_i
    # round up to a factor of the step
    # return val + (@brightness_step - (val % @brightness_step))
    #    return val
  end
  
  def setval(value)
    #bright = val * 255 / 100
    system("spicctrl -b #{value}")
  end
end

rsjog = RSJog.new
