require 'maglev/method_source'
module WebTools
  # Provide information about the Ruby environment
  module Ruby
    # Traverse the Ruby namespace hierarchy and execute block for all classes
    # and modules.  Returns an IdentitySet of all classes and modules found.
    # Skips autoloads (i.e., does not trigger them and does not yield them to
    # the block).
    #
    # @param [Module] klass The Class or Module object to start traversal.
    #         Default is Object.
    #
    # @param [IdentitySet] rg The recursion guard used to prevent infinite
    #         loops; also used as return value.
    #
    # @return [IdentitySet] An IdentitySet of all the Classes and Modules
    #         registered in the Ruby namespace
    #
    def each_module(klass=Object, rg=IdentitySet.new, &block)
      raise 'Must be class or module' unless Module === klass
      unless rg.include?(klass)
        rg.add klass
        yield klass
        klass.constants.each do |c|
          unless klass.autoload?(c)
            obj = klass.const_get c
            each_module(obj, rg, &block) if Module === obj
          end
        end
      end
      rg
    end
    module_function :each_module

    # Returns a sorted list of class and module names in the Ruby Namespace
    #
    # @return [Array] sorted list of class and module names found in the Ruby
    #         namespace hierarchy.
    def class_and_module_names
      names = []
      each_module { |klass|  names << klass.name }
      names.sort
    end
    module_function :class_and_module_names

    # A simple message from Ruby Land.
    def say_something
      "Hello from MagLev, My name is '#{self.name}'"
    end
    module_function :say_something

    # Return an object named in the Ruby namespace.
    #
    # @param [String] name The name of the object. E.g., "Object",
    #         "Errno::EACCES", "Foo::Bar::Baz".
    #
    # @return [Object] the named object.
    #
    # @raise [NameError] if the name can't be found
    def find_in_namespace(name)
      name.split('::').inject(Object) do |parent, name|
        obj = parent.const_get name
      end
    end
    module_function :find_in_namespace

    MODULE_MOD_FNS = Module.instance_methods(false)

    def module_info_for(name)
      mod = find_in_namespace name
      { :constants        => mod.constants.sort,
        :class_methods    => module_fns_for(mod),
        :instance_methods => mod.instance_methods(false).sort }
    end
    module_function :module_info_for

    def module_fns_for(mod)
      res = []
      begin
        sclass = mod.__singleton_class
        res = sclass.instance_methods(false) - MODULE_MOD_FNS
      rescue => e
        # nothing
      end
      res
    end
    module_function :module_fns_for

    # Get the source code for the named class and method
    #
    # The success of this method depends on being called from a Ruby stack,
    # not a Smalltalk stack.
    #
    # @param [String] The name of the class
    # @param [String] The method name
    # @param [Boolean] if true, then look for a class method, otherwise look
    #         for an instance method.
    def source_for(class_name, method_name, instance_method=true)
      klass = find_in_namespace(class_name)
      gsnmeth = klass.gs_method_for(method_name, instance_method)
      src = gsnmeth.__source_string
      file,line = gsnmeth.__source_location
      file.nil? ? "#{src}\n# No file information available." : "#{src}\n##{file}:#{line}"
    end
    module_function :source_for

    # Return a display string suitable for rendering a constant.
    # Currently, it just returns the results of #inspect on the object.
    #
    # TODO
    #   1. If the const value is a proc, then return "Proc: " + the source code.
    #
    # @param [String,Symbol] class_name The name of the class to query.
    # @param [String,Symbol] const_name The name of the constant.
    # @return [String] A description of the constant.
    #
    def const_info_for(class_name, const_name)
      klass = find_in_namespace class_name
      const = klass.const_get const_name
      const.inspect
    end
    module_function :const_info_for

    # def logit(msg)
    #   File.open("/tmp/log", "a+") do |f|
    #     f.puts msg
    #     f.flush
    #   end
    # end
    # module_function :logit
  end
end