module Erector
  module Needs
    NO_DUP_CLASSES = [NilClass, FalseClass, TrueClass, 0.class, Float, Symbol].freeze

    def self.included(base)
      base.extend ClassMethods
    end

    module ClassMethods
      # Class method by which widget classes can declare that they need certain
      # parameters. If needed parameters are not passed in to #new, then an
      # exception will be thrown (with a hopefully useful message about which
      # parameters are missing). This is intended to catch silly bugs like
      # passing in a parameter called 'name' to a widget that expects a
      # parameter called 'title'.
      #
      # You can also declare default values for parameters using hash syntax.
      # You can put #needs declarations on multiple lines or on the same line;
      # the only caveat is that if there are default values, they all have to be
      # at the end of the line (so they go into the magic hash parameter).
      #
      # If a widget has no #needs declaration then it will accept any
      # combination of parameters just like normal. If a widget wants to declare
      # that it takes no parameters, use the special incantation "needs nil"
      # (and don't declare any other needs, or kittens will cry).
      #
      # Usage:
      #    class FancyForm < Erector::Widget
      #      needs :title, :show_okay => true, :show_cancel => false
      #      ...
      #    end
      #
      # That means that
      #   FancyForm.new(:title => 'Login')
      # will succeed, as will
      #   FancyForm.new(:title => 'Login', :show_cancel => true)
      # but
      #   FancyForm.new(:name => 'Login')
      # will fail.
      #
      def needs(*args)
        args.each do |arg|
          (@needs ||= []) << (arg.nil? ? nil : (arg.is_a? Hash) ? arg : arg.to_sym)
        end
      end

      def get_needs
        @needs ||= []

        ancestors[1..-1].inject(@needs.dup) do |needs, ancestor|
          needs.push(*ancestor.get_needs) if ancestor.respond_to?(:get_needs)
          needs
        end
      end

      def needed_variables
        @needed_variables ||= get_needs.map{|need| need.is_a?(Hash) ? need.keys : need}.flatten
      end

      def needed_defaults
        @needed_defaults ||= get_needs.inject({}) do |defaults, need|
          defaults = need.merge(defaults) if need.is_a? Hash
          defaults
        end
      end

      def needs?(name)
        needed_variables.empty? || needed_variables.include?(name)
      end

      def ignore_extra_assigns
        @ignore_extra_assigns ||= false
      end

      def ignore_extra_assigns=(new_value)
        @ignore_extra_assigns = (new_value ? :true : :false)
      end
    end

    def initialize(assigns = {})
      super

      assigned = assigns.keys

      # set variables with default values
      self.class.needed_defaults.each do |name, value|
        unless assigned.include?(name)
          value = NO_DUP_CLASSES.include?(value.class) ? value : value.dup
          instance_variable_set("@#{name}", value)
          assigned << name
        end
      end

      missing = self.class.needed_variables - assigned
      unless missing.empty? || missing == [nil]
        raise ArgumentError, "Missing parameter#{missing.size == 1 ? '' : 's'} for #{self.class.name}: #{missing.join(', ')}"
      end

      excess = assigned - self.class.needed_variables
      unless self.class.needed_variables.empty? || excess.empty? || self.class.ignore_extra_assigns
        raise ArgumentError, "Excess parameter#{excess.size == 1 ? '' : 's'} for #{self.class.name}: #{excess.join(', ')}"
      end
    end
  end
end
