# class_eval, instance_eval, define_singleton_method, and define_method

# class_eval works on a class the same as if you were opening it to add new behaviour to it.
# Adding a class method
class Game; end

Game.class_eval do
  def self.find_by_owner(name)
    puts 'hello'
  end
end

Game.find_by_owner('caroline')
# hello

# Adding an instance method dynamically
class LibraryManager
  def self.make_available(klass, user)
    klass.class_eval do
      define_method("lend_to_#{user}") do
        puts 'wow'
      end
    end
  end
end

LibraryManager.make_available(Game, "caro")
game = Game.new
game.lend_to_caro
# wow

# Adding a class method dynamically
class LibraryManager
  def self.make_available(klass, user)
    klass.class_eval do
      define_singleton_method("lend_to_#{user}") do
        puts 'wow'
      end
    end
  end
end

LibraryManager.make_available(Game, "caro")
Game.lend_to_caro
# wow

# instance_eval executes code in the block as if inside an instance method.
# The 'self' in that block is the instance of the object.
class KlassWithSecret
  def initialize
    @secret = 99
  end
end
k = KlassWithSecret.new
k.instance_eval { @secret }
# => 99

# run an optional block on the instance when it is initialized
class Game
  def initialize(&block)
    instance_eval(&block) if block_given? # same as self.instance_eval...
  end
end

Game.new do
  puts 'fuck'
end

# Method missing will trap calls to non-existent _instance_ methods
class Library
  def method_missing(name)
    puts name
  end
end

lib = Library.new.non_existent_method
# non_existent_method
# => nil

# Library.non_existent_class_method
# if you uncomment line above, you will get:
# more_ruby.rb:80:in `<main>': undefined method `non_existent_class_method' for Library:Class (NoMethodError)

# If you want to trap calls to non-existent _class_ methods, use self.method_missing.
# Here we are re-opening the class, so we will still trap calls to non-existent _instance_ methods as well.
class Library
  def self.method_missing(name)
    puts name
  end
end

Library.non_existent_class_method
# non_existent_class_method
# => nil
lib = Library.new.non_existent_method # that still works
# non_existent_method
# => nil

class Example
  def method_missing(method_name, *arguments, &block)
    puts "I need this instance method: #{method_name}"
  end

  def self.method_missing(method_name, *arguments, &block)
    puts "I need this class method: #{method_name}"
  end

  # define_method only create instance methods
  # Want class methods? Use define_singleton_method, which works with _anything_.
  # http://apidock.com/ruby/Object/define_singleton_method
  define_method('say_hi_class') do
    puts 'Hi'
  end
end

Example.some_class_method
# I need this class method: some_class_method
Example.new.some_instance_method
# I need this instance method: some_instance_method
Example.say_hi_class
# I need this class method: say_hi_class
Example.new.say_hi_class
# Hi

# define_singleton_method will work on any object:
class A
  class << self
    def class_name
      name
    end
  end
end

A.define_singleton_method(:who_am_i) do
  puts "I am: #{class_name}"
end

A.who_am_i
# "I am: A"

girl = "Caro"
girl.define_singleton_method(:hello) do
  puts "#{self}: Hello there!"
end
girl.hello
# "Caro: Hello there!"

# How to delegate to an object from method_missing:
class Manager
  def hello
    puts "hello back at you"
  end
end

class Library
  def initialize(console)
    @manager = console
  end

  def method_missing(name, *args)
    @manager.send(name, *args)
  end
end

manager = Manager.new
library = Library.new(manager)
library.hello
# hello back at you

# Delegate conditionnally:
class Library
  def initialize(console)
    @manager = console
  end

  def method_missing(name, *args)
    if name.to_s =~ /atari/
      @manager.send(name, *args)
    else
      super
    end
  end
end
