module Boson
  # Gathers method attributes by redefining method_added and capturing method
  # calls before a method.
  class MethodInspector
    METHODS = [:config, :desc, :options]
    SCRAPEABLE_METHODS = [:options]
    METHOD_CLASSES = {:config=>Hash, :desc=>String, :options=>Hash}
    ALL_METHODS = METHODS + [:option]

    def self.safe_new_method_added(mod, meth)
      return unless mod.to_s[/^Boson::Commands::/]
      new_method_added(mod, meth)
    end

    def self.new_method_added(mod, meth)
      instance.new_method_added(mod, meth)
    end

    class << self; attr_accessor :instance end

    def self.instance
      @instance ||= new
    end

    (METHODS + [:option, :mod_store]).each do |meth|
      define_singleton_method(meth) do |*args|
        instance.send(meth, *args)
      end
    end

    attr_accessor :current_module, :mod_store
    def initialize
      @mod_store = {}
    end

    # The method_added used while scraping method attributes.
    def new_method_added(mod, meth)
      self.current_module = mod

      store[:temp] ||= {}
      METHODS.each do |e|
        store[e][meth.to_s] = store[:temp][e] if store[:temp][e]
      end
      if store[:temp][:option]
        (store[:options][meth.to_s] ||= {}).merge! store[:temp][:option]
      end
      during_new_method_added mod, meth
      store[:temp] = {}

      if SCRAPEABLE_METHODS.any? {|m| has_inspector_method?(meth, m) }
        set_arguments(mod, meth)
      end
    end

    METHODS.each do |e|
      define_method(e) do |mod, val|
        (@mod_store[mod] ||= {})[e] ||= {}
        (store(mod)[:temp] ||= {})[e] = val
      end
    end

    # Scrapes option
    def option(mod, name, value)
      (@mod_store[mod] ||= {})[:options] ||= {}
      (store(mod)[:temp] ||= {})[:option] ||= {}
      (store(mod)[:temp] ||= {})[:option][name] = value
    end

    # Hash of a module's method attributes i.e. descriptions, options by method
    # and then attribute
    def store(mod=@current_module)
      @mod_store[mod]
    end

    # Renames store key from old to new name
    def rename_store_key(old, new)
      mod_store[new] = mod_store.delete old
    end

    # Sets current module
    def current_module=(mod)
      @current_module = mod
      @mod_store[mod] ||= {}
    end

    module API
      # Method hook called during new_method_added
      def during_new_method_added(mod, meth); end

      def set_arguments(mod, meth)
        store[:args] ||= {}

        args = mod.instance_method(meth).parameters.map do|(type, name)|
          case type
          when :rest then ["*#{name}"]
          when :req  then [name.to_s]
          when :opt  then [name.to_s, '']
          else nil
          end
        end.compact

        store[:args][meth.to_s] = args
      end

      # Determines if method's arguments should be scraped
      def has_inspector_method?(meth, inspector)
        true
      end
    end
    include API
  end
end
