module Draper
  class Base
    require 'active_support/core_ext/class/attribute'
    class_attribute :denied, :allowed, :model_class
    attr_accessor :context, :model

    DEFAULT_DENIED = Object.new.methods << :method_missing
    FORCED_PROXY = [:to_param]
    FORCED_PROXY.each do |method|
      define_method method do |*args, &block|
        model.send method, *args, &block
      end
    end
    self.denied = DEFAULT_DENIED

    # Initialize a new decorator instance by passing in
    # an instance of the source class. Pass in an optional
    # context is stored for later use.
    #
    # @param [Object] instance to wrap
    # @param [Object] context (optional)
    def initialize(input, context = {})
      input.inspect # forces evaluation of a lazy query from AR
      input.inspect
      self.class.model_class = input.class if model_class.nil?
      @model = input
      self.context = context
    end

    # Proxies to the class specified by `decorates` to automatically
    # lookup an object in the database and decorate it.
    #
    # @param [Symbol or String] id to lookup
    # @return [Object] instance of this decorator class
    def self.find(input, context = {})
      self.new(model_class.find(input), context)
    end

    # Typically called within a decorator definition, this method
    # specifies the name of the wrapped object class.
    #
    # For instance, a `ProductDecorator` class might call `decorates :product`
    #
    # But they don't have to match in name, so a `EmployeeDecorator`
    # class could call `decorates :person` to wrap instances of `Person`
    #
    # This is primarilly set so the `.find` method knows which class 
    # to query.
    #
    # @param [Symbol] class_name snakecase name of the decorated class, like `:product`
    def self.decorates(input)
      self.model_class = input.to_s.camelize.constantize
      model_class.send :include, Draper::ModelSupport
    end

    # Specifies a black list of methods which may *not* be proxied to
    # to the wrapped object.
    #
    # Do not use both `.allows` and `.denies` together, either write
    # a whitelist with `.allows` or a blacklist with `.denies`
    #
    # @param [Symbols*] methods to deny like `:find, :find_by_name`
    def self.denies(*input_denied)
      raise ArgumentError, "Specify at least one method (as a symbol) to exclude when using denies" if input_denied.empty?
      raise ArgumentError, "Use either 'allows' or 'denies', but not both." if self.allowed?
      self.denied += input_denied
    end

    # Specifies a white list of methods which *may* be proxied to
    # to the wrapped object. When `allows` is used, only the listed
    # methods and methods defined in the decorator itself will be
    # available.
    #
    # Do not use both `.allows` and `.denies` together, either write
    # a whitelist with `.allows` or a blacklist with `.denies`
    #
    # @param [Symbols*] methods to allow like `:find, :find_by_name`
    def self.allows(*input_allows)
      raise ArgumentError, "Specify at least one method (as a symbol) to allow when using allows" if input_allows.empty?
      raise ArgumentError, "Use either 'allows' or 'denies', but not both." unless (self.denied == DEFAULT_DENIED)
      self.allowed = input_allows
    end

    # Initialize a new decorator instance by passing in
    # an instance of the source class. Pass in an optional
    # context is stored for later use.
    #
    # When passing in a single object, using `.decorate` is
    # identical to calling `.new`. However, `.decorate` can
    # also accept a collection and return a collection of
    # individually decorated objects.
    #
    # @param [Object] instance(s) to wrap
    # @param [Object] context (optional)
    def self.decorate(input, context = {})
      input.respond_to?(:each) ? input.map{|i| new(i, context)} : new(input, context)
    end

    # Access the helpers proxy to call built-in and user-defined
    # Rails helpers. Aliased to `.h` for convinience.
    #
    # @return [Object] proxy   
    def helpers
      Thread.current[:current_view_context]
    end
    alias :h :helpers

    # Calling `lazy_helpers` will make the built-in and
    # user-defined Rails helpers accessible as class methods
    # in the decorator without using the `h.` or `helpers.` proxy.
    #
    # The drawback is that you dump many methods into your decorator's
    # namespace and collisions could create unexpected results.
    def self.lazy_helpers
      self.send(:include, Draper::LazyHelpers)
    end

    # Use primarily by `form_for`, this returns an instance of 
    # `ActiveModel::Name` set to the wrapped model's class name
    #
    # @return [ActiveModel::Name] model_name   
    def self.model_name
      ActiveModel::Name.new(model_class)
    end

    # Fetch the original wrapped model.
    #
    # @return [Object] original_model
    def to_model
      @model
    end

    # Delegates == to the decorated models
    #
    # @return [Boolean] true if other's model == self's model 
    def ==(other)
      @model == other.model
    end

    def respond_to?(method)
      if select_methods.include?(method)
        model.respond_to?(method)
      else
        super
      end
    end

    def method_missing(method, *args, &block)
      if select_methods.include?(method)
        model.send(method, *args, &block)
      else
        super
      end
    end

  private
    def select_methods
      specified = self.allowed || (model.public_methods.map{|s| s.to_sym} - denied.map{|s| s.to_sym})
      (specified - self.public_methods.map{|s| s.to_sym}) + FORCED_PROXY
    end
  end
end