require 'object_browser_ui'
require 'gtk2'

Thread.abort_on_exception = true

module ObjectBrowser
  module UI
    module Gtk
      include ::Gtk
      
      module Value
        TYPE = 0
        TEXT = 1
        ADDITIONAL_TEXT = 2
        OBJECT = 3
      end

      # Tree View Column that can change its display depending on the type of
      # the displayed row.
      class FormatableTreeViewColumn < TreeViewColumn
        def initialize(*args)
          super(*args)
          @formats = {}
          set_format(:h1,       Gtk::Stock::REMOVE,       '#000000')
          set_format(:h2,       Gtk::Stock::REMOVE,       '#111111')
          set_format(:h3,       Gtk::Stock::REMOVE,       '#222222')
          set_format(:variable, Gtk::Stock::SELECT_FONT,  '#cd0000')
          set_format(:constant, Gtk::Stock::BOLD,         '#8b0000')
          set_format(:method,   Gtk::Stock::APPLY,        '#8b2323')
          set_format(:module,   Gtk::Stock::YES,          '#5500B4')
          set_format(:class,    Gtk::Stock::YES,          '#0000c4')
          set_format(:object,   Gtk::Stock::NO,           'black')
          set_default_format(   Gtk::Stock::GO_FORWARD,   'black')
        end

        # Set the format for some type of line
        #
        # E.g.
        #   set_format(:class, Stock::YES, 'blue')
        def set_format(type, *args)
          @formats[type.to_sym] = args
        end

        # Set the default format
        def set_default_format(*args)
          @formats.default = *args
        end
        
        # Get the format information for a type of row.
        def format(type)
          @formats[type.to_sym]
        end
      end
      
      # A column that contains a pixbuffer followed by a text renderer. 
      class PixTextColumn < FormatableTreeViewColumn
        # Initialize column with a title and the index of the pixbuffer in the
        # iterator and the text index in the iterator.
        def initialize(title = '', pix_index = 0, text_index = 1)
          super(title)

          pix = Gtk::CellRendererPixbuf.new
          self.pack_start(pix, false)
          set_cell_data_func(pix) do |tvc, cell, model, iter|
            pix.stock_id = format(iter[Value::TYPE])[0]
          end

          text = Gtk::CellRendererText.new
          self.pack_start(text, true)
          set_cell_data_func(text) do |tvc, cell, model, iter|
            text.text = iter[Value::TEXT]
            text.foreground = format(iter[Value::TYPE])[1]
          end
          add_attribute(text, :text, text_index)
        end
      end

      # A column that contains only a text
      class TextColumn < FormatableTreeViewColumn
        attr_reader :text_renderer
        
        # initialize with column title and the index of the text in the iterator.
        def initialize(title = '', text_index = 0)
          super(title)

          @text_renderer = Gtk::CellRendererText.new
          self.pack_start(@text_renderer, true)
          add_attribute(@text_renderer, :text, text_index)
        end
      end

      class BrowserTree < TreeView
        # Create a new tree. The default model contains a pixbuf, string, string and object.
        def initialize(model = Gtk::TreeStore.new(Symbol, String, String, Object))
          super(model)

          set_rules_hint(true)
          
          column1 = PixTextColumn.new('How to call this?', Value::TYPE, Value::TEXT)
          column2 = TextColumn.new('Additional', Value::ADDITIONAL_TEXT)
          append_column(column1)
          append_column(column2)
          column1.resizable = true
          #column2.resizable = true
          column1.alignment = 0.5
          column2.alignment = 0.5
        end

        # Empty the tree
        def clear
          self.model.clear
        end

        # Add a node to the model. If parent is nil the node will be added as a root node.
        # Returns an iterator that point to the inserted node.
        def insert_node(parent, type, text, additional = '', object = nil)
          text = text.to_s
          additional = additional.to_s if additional
          iter = self.model.append(parent)
          iter.set_value(Value::TYPE, type)
          iter.set_value(Value::TEXT, text)          
          iter.set_value(Value::ADDITIONAL_TEXT, additional) if additional
          iter.set_value(Value::OBJECT, object) if object
          iter
        end

        # Clear the tree, freeze it and call a block in which the tree can be filled.
        def change_tree(*args)
          begin
            self.model.freeze_notify
            yield(self, *args)
          ensure
            self.model.thaw_notify
          end
          self
        end
      end

      # Lists all classes and all objects with their class.
      class ClassBrowser < BrowserTree
        def initialize(classtree, object, *args)
          @object_selected = lambda{}
          super(*args)
          signal_connect('row-activated') do | treeview, path, column |
            iter = self.model.get_iter(path)
            @object_selected.call(iter[3], iter)
          end
          signal_connect('row-expanded') do | treeview, iter, path | beautify(iter) end
          update_tree(classtree, object)
        end

        # Set on_object_selected callback
        def on_object_selected(&callback)
          @object_selected = callback
        end

        # Load a new classtree into the class browser
        def update_tree(classtree, object = nil)
          self.change_tree do
            self.clear
            self.fill_model(classtree, nil)
            self.find_object(object) 
          end
        end

        # Expand tree to object and select object
        def find_object(object)
          iter = model.get_iter('0')
          object.class.ancestors.reverse.each do | klass |
            () while iter[Value::OBJECT] != klass and iter.next!
            expand_row(iter.path, false)
            iter = iter.first_child
          end
          () while iter[Value::OBJECT] != object and iter.next!
          expand_row(iter.path, false)
          set_cursor(iter.path, nil, nil)
          iter.path
        end
        
        protected
        # Fill the class tree from a classtree structure
        def fill_model(classnode = ::ObjectBrowser::create_class_tree(), parent = nil)
          if classnode.klass.is_a?Class
            node = insert_node(parent, :class, classnode.klass.to_s, "#<#{classnode.klass.id.to_s(16)}>", classnode.klass)
          elsif classnode.klass.is_a?Module
            node = insert_node(parent, :module, classnode.klass.to_s, "#<#{classnode.klass.id.to_s(16)}>", classnode.klass)
          else
            raise 'This should not happen. Expected the class of classnode to be either a class or a module'
          end
          classnode.objects.each do | object |
            insert_node(node, :object, "#<#{object.id.to_s(16)}>",  "#<#{object.id.to_s(16)}>", object)
          end
          classnode.subclasses.to_a.sort_by{|k,s| k.to_s}.each do | klass, subclass | fill_model(subclass, node) end
        end

        protected
        # Get detailed information about each child of iter and update its presentation. This happens in a thread, such that
        # browsing of the tree can continue. beautify is called, after part of a tree has been expanded.
        #
        # Todo: Check if this is thread safe.
        def beautify(iterator)
          Thread.new(iterator) do | iter |
            iter = iter.first_child
            begin
              object = iter[Value::OBJECT]
              iter[Value::TEXT] = DEFAULT_DESCRIBER.display_string(object, 64).inspect[1..-2]
            end while iter.next!
          end
        end
      end

      class ObjectTreeView < BrowserTree
        # TODO: Factor this out such that I only have to write:
        # attr_updater [:show_empty, :rebuild_tree], [:show_methods, :rebuild_tree], [:show_state, :rebuild_tree]
        attr_reader :show_empty, :show_methods, :show_state
        def show_empty=(value)   @show_empty = value;   rebuild_tree; end
        def show_methods=(value) @show_methods = value; rebuild_tree; end 
        def show_state=(value)   @show_state = value;   rebuild_tree; end
        
        def initialize(object, *args)
          super(*args)
          @show_empty = false
          @show_methods = true
          @show_state = true

          signal_connect('row-activated') do | tree, path, column |
            if row_expanded?(path)
              collapse_row(path, false)
            else
              expand_row(path, false)
            end
          end
          signal_connect('row-expanded') do | tree_view, iter, path |
            object = iter[Value::OBJECT]
            child = iter.first_child
            if !child[0] and object # Expand object for the first time.
              update_tree(object, iter)
              self.model.remove(child) # Remove dummy child
            end
          end
          update_tree(object)
        end

        # Update the tree information by expanding a model. A parent of nil inserts the object into the root.
        def update_tree(object, parent = nil)
          self.change_tree do
            self.clear unless parent
            self.fill_model(object, parent)            
            expand_row(Gtk::TreePath.new("0"), false)
            if iter = model.get_iter('0:0')
              begin
                expand_row(iter.path, false)
              end while iter.next!
            end
          end
        end

        protected
        def fill_model(object, parent = nil)
          @object = object unless parent # Remember to which object we are pointing
          if parent
            description_factory = GTKDescriptionFactory.new(self, parent)
          else
            description_factory = GTKDescriptionFactory.new(self, nil)
            description_factory = case object
                                  when Class: description_factory.add_section(:class, DEFAULT_DESCRIBER.short_description(object))
                                  when Module: description_factory.add_section(:module, DEFAULT_DESCRIBER.short_description(object))
                                  else description_factory.add_section(:object, DEFAULT_DESCRIBER.short_description(object))
                                  end
          end
          DEFAULT_DESCRIBER.show_empty_sections = @show_empty
          DEFAULT_DESCRIBER.show_methods = @show_methods
          DEFAULT_DESCRIBER.show_state = @show_state
          DEFAULT_DESCRIBER.describe(object, description_factory)
        end
        
        protected
        def rebuild_tree
          update_tree(@object)
        end
      end

      class ObjectDescription < Gtk::TextView
        def initialize(object = nil, *args)
          super(*args)
          update(object) if object
        end
        
        def update(object)
          @object = object
          self.buffer.text = DEFAULT_DESCRIBER.short_description(@object)
        end
      end
      
      class ObjectBrowserWindow < Dialog
        
        TOGGLE_EMPTY_SECTIONS = 1
        TOGGLE_STATE = 2
        TOGGLE_METHODS =  3

        def initialize(object)
          super("Object Browser for Ruby", nil, 
                MODAL|NO_SEPARATOR,
                ['Show/Hide Empty Entrys', TOGGLE_EMPTY_SECTIONS],
                ['Show/Hide State', TOGGLE_STATE],
                ['Show/Hide Methods', TOGGLE_METHODS],
                [Gtk::Stock::QUIT, RESPONSE_CLOSE])

          GC.start
          classbrowser = ClassBrowser.new(::ObjectBrowser::create_class_tree(), object)
          object_description = ObjectDescription.new(object)
          object_browser = ObjectTreeView.new(object)

          hpane = Gtk::HPaned.new()
          vbox.add(hpane)
          hpane.pack1(Gtk::ScrolledWindow.new.add(classbrowser), true, true)
          vpane = Gtk::VPaned.new()
          hpane.pack2(vpane, true, true)
          vpane.pack1(object_description, false, true)
          vpane.pack2(Gtk::ScrolledWindow.new.add(object_browser), true, true)

          #signal_connect(:size_allocate) do |widget, allocation|
            hpane.position = 260#; allocation.width / 3
          #end

          classbrowser.on_object_selected do | object, iter | object_browser.update_tree(object); object_description.update(object) end
          
          signal_connect(:response) do |widget, response|
            case response
            when TOGGLE_EMPTY_SECTIONS: object_browser.show_empty = !object_browser.show_empty
            when TOGGLE_STATE: object_browser.show_state = !object_browser.show_state
            when TOGGLE_METHODS: object_browser.show_methods = !object_browser.show_methods
            when RESPONSE_CLOSE: destroy; Gtk.main_quit
            end
          end
          self.show_all    
        end
      end

      def browse(object)
        ::Gtk.init
        win = ObjectBrowserWindow.new(object).set_default_size(400, 400).show_all
        ::Gtk.main
      end

      class GTKDescriptionFactory < ObjectBrowser::UI::DescriptionFactory
        def initialize(object_browser, parent)
          @object_browser = object_browser
          @parent = parent
        end
        
        def add(type, text, object = nil, additional = '')
          result = self.class.new(@object_browser, node = @object_browser.insert_node(@parent, type, text, additional, object))
          @object_browser.model.append(node) if object
          result
        end
        
        def add_section(type, text)
          add(type, text)
        end
      end
      extend self
    end
  end
  
  DEFAULT_DESCRIBER = Describer.new()
end