#!/usr/bin/env ruby
#
# This is a quite complex example using internal lower level API.
# Not a good starting point, but might be usefull if you want to do tricky
# stuff.
# -- Arnaud

require "dbus"
require "gtk2"

ENABLE_SYSTEM = false

class MethodCallWindow
  def initialize(pwindow, intf, meth)
    @intf = intf
    @meth = meth
    @entries = []
    @dialog = Gtk::Dialog.new(meth.name, pwindow,
                              Gtk::Dialog::MODAL | Gtk::Dialog::NO_SEPARATOR,
                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])

    @meth.params.each do |param|
      shbox = Gtk::HBox.new(true, 0)
      label = Gtk::Label.new("#{param[0]} (#{param[1]})")
      input = Gtk::Entry.new
      @entries << input
      shbox.pack_start(label, true, true, 0)
      shbox.pack_start(input, true, true, 0)
      @dialog.vbox.pack_start(shbox, true, true, 0)
      @dialog.vbox.show_all
    end
  end

  def run
    on_ok if @dialog.run == Gtk::Dialog::RESPONSE_OK
    @dialog.destroy
  end

  def on_ok
    bus = @intf.object.bus
    m = DBus::Message.new(DBus::Message::METHOD_CALL)
    m.path = @intf.object.path
    m.interface = @intf.name
    m.destination = @intf.object.destination
    m.member = @meth.name
    m.sender = bus.unique_name
    @meth.params.each_with_index do |param, idx|
      entry = @entries[idx]
      data = nil
      case param[1]
      when "u", "i"
        data = entry.text.to_i
      when "s"
        data = entry.text
      when /^a/
        begin
          data = eval(entry.text)
        rescue
          puts "Incorrect data: #{data}"
        end
      end
      m.add_param(param[1], data)
    end
    bus.send_sync_or_async(m) do |retm|
      if retm.is_a?(DBus::Error)
        puts "Error: #{retm.inspect}"
      else
        puts "Method #{m.member} returns: #{retm.params.inspect}"
      end
    end
  end
end

class DBusUI
  def initialize
    @glade = Gtk::Builder.new
    @glade << "gdbus.glade"

    @sessiontreeview = @glade.get_object("sessiontreeview")
    setup_treeview_renderer(@sessiontreeview, "D-Bus Objects")
    @sessiontreeview.selection.signal_connect("changed") do |selection|
      on_treeview_selection_changed(selection)
    end

    @systemtreeview = @glade.get_object("systemtreeview")
    setup_treeview_renderer(@systemtreeview, "D-Bus Objects")
    @systemtreeview.selection.signal_connect("changed") do |selection|
      on_treeview_selection_changed(selection)
    end

    @methsigtreeview = @glade.get_object("methsigtreeview")
    # ierk
    setup_methodview_renderer(@methsigtreeview)
    @methsigtreeview.signal_connect("row-activated") do |view, path, column|
      on_method_activated(view, path, column)
    end

    @window = @glade.get_object("window1")
    @window.show_all
    start_buses
  end

  def beautify_method(meth)
    # Damn, this need to be rewritten :p
    s = meth.name + "("
    if meth.is_a?(DBus::Method)
      s += (meth.params.collect { |a| "in #{a[0]}:#{a[1]}" } +
            meth.rets.collect { |a| "out #{a[0]}:#{a[1]}" }).join(", ")
    elsif meth.is_a?(DBus::Signal)
      s += (meth.params.collect { |a| "in #{a[0]}:#{a[1]}" }).join(", ")
    end
    s += ")"
    s
  end

  def on_treeview_selection_changed(selection)
    selected = selection.selected
    model = Gtk::ListStore.new(String, String, DBus::Method,
                               DBus::ProxyObjectInterface)
    @methsigtreeview.model = model
    if selected
      if (intf = selected[1])
        intf.methods.keys.sort.each do |mi|
          m = intf.methods[mi]
          subiter = model.append
          subiter[0] = beautify_method(m)
          subiter[1] = "M"
          subiter[2] = m
          subiter[3] = intf
        end
        intf.signals.keys.sort.each do |mi|
          m = intf.signals[mi]
          subiter = model.append
          subiter[0] = beautify_method(m)
          subiter[1] = "S"
          subiter[2] = m
          subiter[3] = intf
        end
      end
    end
  end

  def on_method_activated(view, path, _column)
    name = view.model.get_iter(path)[0]
    puts "Clicked on: #{name.inspect}"
    type = view.model.get_iter(path)[1]
    if type == "M"
      method = view.model.get_iter(path)[2]
      intf = view.model.get_iter(path)[3]
      MethodCallWindow.new(@window, intf, method).run
    elsif type == "S"
      signal = view.model.get_iter(path)[2]
      intf = view.model.get_iter(path)[3]
      mr = DBus::MatchRule.new.from_signal(intf, signal)
      puts "*** Registering matchrule: #{mr} ***"
      intf.object.bus.add_match(mr) do |sig|
        puts "Got #{sig.member}(#{sig.params.join(",")})"
      end
    end
  end

  def on_sessiontreeview_row_activated(view, path, _column)
    name = view.model.get_iter(path)[0]
    puts "Clicked on: #{name.inspect}"
  end

  def on_window_delete_event(_window, _event)
    Gtk.main_quit
  end

  def setup_methodview_renderer(treeview)
    renderer = Gtk::CellRendererText.new
    _col_offset = treeview.insert_column(-1, "T", renderer, "text" => 1)
    col_offset = treeview.insert_column(-1, "Name", renderer, "text" => 0)
    column = treeview.get_column(col_offset - 1)
    column.clickable = true
  end

  def setup_treeview_renderer(treeview, str)
    renderer = Gtk::CellRendererText.new
    col_offset = treeview.insert_column(-1, str, renderer, "text" => 0)
    column = treeview.get_column(col_offset - 1)
    column.clickable = true
  end

  def process_input(bus)
    # THIS is the bad ass loop
    # we should return to the glib main loop from time to time. Anyone with a
    # proper way to handle it ?
    bus.update_buffer
    bus.messages.each do |msg|
      bus.process(msg)
    end
  end

  def start_buses
    # call glibize to get dbus messages from the glib mainloop
    DBus::SessionBus.instance.glibize
    DBus::SystemBus.instance.glibize if ENABLE_SYSTEM

    DBus::SessionBus.instance.proxy.ListNames do |_msg, names|
      fill_treeview(DBus::SessionBus.instance, @sessiontreeview, names)
    end

    return unless ENABLE_SYSTEM
    DBus::SystemBus.instance.proxy.ListNames do |_msg, names|
      fill_treeview(DBus::SystemBus.instance, @systemtreeview, names)
    end
  end

  def walk_node(model, iter, node)
    node.each_pair do |key, val|
      subiter = model.append(iter)
      subiter[0] = key
      walk_node(model, subiter, val)
    end

    return if node.object.nil?
    node.object.interfaces.sort.each do |ifname|
      subiter = model.append(iter)
      subiter[0] = ifname
      subiter[1] = node.object[ifname]
    end
  end

  def introspect_services(model, bus)
    el = @introspect_array.shift
    if !(el =~ /^:/)
      iter = model.append(nil)
      iter[0] = el
      puts "introspecting: #{el}"
      begin
        service = bus.service(el).introspect
        walk_node(model, iter, service.root)
      rescue Exception => e
        puts "DBus Error:"
        puts e.backtrace.join("\n")
      end
    end

    !@introspect_array.empty?
  end

  def fill_treeview(bus, treeview, array)
    model = Gtk::TreeStore.new(String, DBus::ProxyObjectInterface)
    treeview.model = model
    @introspect_array = array.sort
    Gtk.idle_add { introspect_services(model, bus) }
  end

  def main
    Gtk.main
  end
end

DBusUI.new.main