# (C) John Mair (banisterfiend) 2011
# MIT License

direc = File.dirname(__FILE__)

require "#{direc}/method_source/version"
require "#{direc}/method_source/source_location"
require "#{direc}/method_source/code_helpers"

module MethodSource
  extend MethodSource::CodeHelpers

  # An Exception to mark errors that were raised trying to find the source from
  # a given source_location.
  #
  class SourceNotFoundError < StandardError; end

  # Helper method responsible for extracting method body.
  # Defined here to avoid polluting `Method` class.
  # @param [Array] source_location The array returned by Method#source_location
  # @param [String]  method_name
  # @return [String] The method body
  def self.source_helper(source_location, name=nil)
    raise SourceNotFoundError, "Could not locate source for #{name}!" unless source_location
    file, line = *source_location

    expression_at(lines_for(file), line)
  rescue SyntaxError => e
    raise SourceNotFoundError, "Could not parse source for #{name}: #{e.message}"
  end

  # Helper method responsible for opening source file and buffering up
  # the comments for a specified method. Defined here to avoid polluting
  # `Method` class.
  # @param [Array] source_location The array returned by Method#source_location
  # @param [String]  method_name
  # @return [String] The comments up to the point of the method.
  def self.comment_helper(source_location, name=nil)
    raise SourceNotFoundError, "Could not locate source for #{name}!" unless source_location
    file, line = *source_location

    comment_describing(lines_for(file), line)
  end

  # Load a memoized copy of the lines in a file.
  #
  # @param [String]  file_name
  # @param [String]  method_name
  # @return [Array<String>]  the contents of the file
  # @raise [SourceNotFoundError]
  def self.lines_for(file_name, name=nil)
    @lines_for_file ||= {}
    @lines_for_file[file_name] ||= File.readlines(file_name)
  rescue Errno::ENOENT => e
    raise SourceNotFoundError, "Could not load source for #{name}: #{e.message}"
  end

  # Clear cache.
  def self.clear_cache
    @lines_for_file = {}
  end

  # @deprecated — use MethodSource::CodeHelpers#complete_expression?
  def self.valid_expression?(str)
    complete_expression?(str)
  rescue SyntaxError
    false
  end

  # @deprecated — use MethodSource::CodeHelpers#expression_at
  def self.extract_code(source_location)
    source_helper(source_location)
  end

  # This module is to be included by `Method` and `UnboundMethod` and
  # provides the `#source` functionality
  module MethodExtensions

    # We use the included hook to patch Method#source on rubinius.
    # We need to use the included hook as Rubinius defines a `source`
    # on Method so including a module will have no effect (as it's
    # higher up the MRO).
    # @param [Class] klass The class that includes the module.
    def self.included(klass)
      if klass.method_defined?(:source) && Object.const_defined?(:RUBY_ENGINE) &&
          RUBY_ENGINE =~ /rbx/

        klass.class_eval do
          orig_source = instance_method(:source)

          define_method(:source) do
            begin
              super
            rescue
              orig_source.bind(self).call
            end
          end

        end
      end
    end

    # Return the sourcecode for the method as a string
    # @return [String] The method sourcecode as a string
    # @raise SourceNotFoundException
    #
    # @example
    #  Set.instance_method(:clear).source.display
    #  =>
    #     def clear
    #       @hash.clear
    #       self
    #     end
    def source
      MethodSource.source_helper(source_location, defined?(name) ? name : inspect)
    end

    # Return the comments associated with the method as a string.
    # @return [String] The method's comments as a string
    # @raise SourceNotFoundException
    #
    # @example
    #  Set.instance_method(:clear).comment.display
    #  =>
    #     # Removes all elements and returns self.
    def comment
      MethodSource.comment_helper(source_location, defined?(name) ? name : inspect)
    end

    # Return the comments associated with the method class/module.
    # @return [String] The method's comments as a string
    # @raise SourceNotFoundException
    #
    # @example
    #  MethodSource::MethodExtensions.method(:included).module_comment
    #  =>
    #     # This module is to be included by `Method` and `UnboundMethod` and
    #     # provides the `#source` functionality
    def class_comment
      if self.respond_to?(:receiver)
        class_inst_or_module =  self.receiver
      elsif self.respond_to?(:owner)
        class_inst_or_module = self.owner
      else
        return comment
      end

      if class_inst_or_module.respond_to?(:name)
        const_name = class_inst_or_module.name
      else
        const_name = class_inst_or_module.class.name
        class_inst_or_module = class_inst_or_module.class
      end

      location = class_inst_or_module.const_source_location(const_name)

      MethodSource.comment_helper(location, defined?(name) ? name : inspect)
    end
    alias module_comment class_comment
  end
end

class Method
  include MethodSource::SourceLocation::MethodExtensions
  include MethodSource::MethodExtensions
end

class UnboundMethod
  include MethodSource::SourceLocation::UnboundMethodExtensions
  include MethodSource::MethodExtensions
end

class Proc
  include MethodSource::SourceLocation::ProcExtensions
  include MethodSource::MethodExtensions
end

