require 'parameters/exceptions'
require 'parameters/class_param'
require 'parameters/instance_param'
require 'parameters/exceptions'
require 'parameters/extensions/meta'

module Parameters
  def self.included(base)
    base.metaclass_eval do
      #
      # @return [Hash]
      #   Parameters for the class.
      #
      def params
        @params ||= {}
      end

      #
      # Sets the values of the class parameters.
      #
      # @param [Hash] values
      #   The names and new values to set the class params to.
      #
      # @example
      #   Test.params = {:x => 5, :y => 2}
      #   # => {:x=>5, :y=>2}
      #
      def params=(values)
        values.each do |name,value|
          if has_param?(name)
            if (value.kind_of?(Parameters::ClassParam) || value.kind_of?(Parameters::InstanceParam))
              value = value.value
            end

            get_param(name).value = value
          end
        end
      end

      #
      # Adds a new parameters to the class.
      #
      # @param [Symbol, String] name
      #   The name of the new parameter.
      #
      # @param [Hash] options
      #   Additional options.
      #
      # @option options [String] :description
      #   The description for the new parameter.
      #
      # @option options [Object, Proc] :default
      #   The default value for the new parameter.
      #
      # @example
      #   parameter 'var'
      #
      # @example
      #   parameter 'var', :default => 3, :description => 'my variable' 
      #
      def parameter(name,options={})
        name = name.to_sym

        # add the parameter to the class params list
        params[name] = Parameters::ClassParam.new(name,options[:description],options[:default])

        # define the reader class method for the parameter
        meta_def(name) do
          params[name].value
        end

        # define the writer class method for the parameter
        meta_def("#{name}=") do |value|
          params[name].value = value
        end

        # define the getter/setter instance methods for the parameter
        attr_accessor(name)
      end

      #
      # Searches for the class parameter with the matching name.
      #
      # @param [Symbol, String] name
      #   The class parameter name to search for.
      #
      # @return [ClassParam]
      #   The class parameter with the matching _name_.
      #
      # @raise [ParamNotFound]
      #   No class parameter with the specified _name_ could be found.
      #
      def get_param(name)
        name = name.to_sym

        ancestors.each do |ancestor|
          if ancestor.include?(Parameters)
            if ancestor.params.has_key?(name)
              return ancestor.params[name]
            end
          end
        end

        raise(Parameters::ParamNotFound,"parameter #{name.to_s.dump} was not found in class #{self.name.dump}",caller)
      end

      #
      # @return [Boolean]
      #   Specifies whether or not there is a class parameter with the
      #   specified _name_.
      #
      def has_param?(name)
        name = name.to_sym

        ancestors.each do |ancestor|
          if ancestor.include?(Parameters)
            return true if ancestor.params.has_key?(name)
          end
        end

        return false
      end

      #
      # Iterates over the parameters of the class and it's ancestors.
      #
      # @yield [param]
      #   The block that will be passed each class parameter.
      #
      def each_param(&block)
        ancestors.each do |ancestor|
          if ancestor.include?(Parameters)
            ancestor.params.each_value(&block)
          end
        end

        return self
      end

      #
      # Returns the description of the class parameters with a given name.
      #
      # @return [String]
      #   Description of the class parameter with the specified name.
      #
      # @raise [ParamNotFound]
      #   No class parameter with the specified name could be found.
      #
      def describe_param(name)
        get_param(name).description
      end

      #
      # Returns the value of the class parameters with a given name.
      #
      # @return [Object]
      #   Value of the class parameter with the specified name.
      #
      # @raise [ParamNotFound]
      #   No class parameter with the specified name could be found.
      #
      def param_value(name)
        get_param(name).value
      end
    end
  end

  #
  # Initalizes the parameters of the object using the given
  # values, which can override the default values of parameters.
  #
  # @param [Hash] values
  #   The names and values to initialize the instance parameters to.
  #
  def initialize_params(values={})
    self.class.each_param do |param|
      # do not override existing instance value if present
      if instance_variable_get("@#{param.name}".to_sym).nil?
        begin
          if param.value.kind_of?(Proc)
            value = if param.value.arity > 0
                      param.value.call(self)
                    else
                      param.value.call()
                    end
          else
            value = param.value.clone
          end
        rescue TypeError
          value = param.value
        end

        instance_variable_set("@#{param.name}".to_sym,value)
      end

      self.params[param.name] = InstanceParam.new(self,param.name,param.description)
    end

    self.params = values if values.kind_of?(Hash)
  end

  #
  # Initializes the parameters using initialize_params. If a +Hash+
  # is passed in as the first argument, it will be used to set the values
  # of parameters described within the Hash.
  #
  def initialize(*args,&block)
    initialize_params(args.first)
  end

  #
  # Adds a new parameter to the object.
  #
  # @param [Symbol, String] name
  #   The name for the new instance parameter.
  #
  # @param [Hash] options
  #   Additional options.
  #
  # @option options [String] :description
  #   The description for the new parameter.
  #
  # @option options [Object, Proc] :default
  #   The default value for the new parameter.
  #
  # @return [InstanceParam]
  #   The newly created instance parameter.
  #
  # @example
  #   obj.parameter('var')
  #
  # @example
  #   obj.parameter('var',:default => 3, :description => 'my variable')
  #
  def parameter(name,options={})
    name = name.to_sym
    default = options[:default]
    description = options[:description]

    # resolve the default value
    if default.kind_of?(Proc)
      value = if default.arity > 0
                default.call(self)
              else
                default.call()
              end
    else
      value = default
    end

    # set the instance variable
    instance_variable_set("@#{name}".to_sym,value)

    # add the new parameter
    self.params[name] = InstanceParam.new(self,name,description)

    instance_eval %{
      # define the reader method for the parameter
      def #{name}
        instance_variable_get("@#{name}".to_sym)
      end

      # define the writer method for the parameter
      def #{name}=(value)
        instance_variable_set("@#{name}".to_sym,value)
      end
    }

    return params[name]
  end

  #
  # @return [Hash]
  #   The parameteres of the class and it's ancestors.
  #
  def class_params
    self.class.params
  end

  #
  # @return [Hash]
  #   The instance parameters of the object.
  #
  def params
    @params ||= {}
  end

  #
  # Sets the values of existing parameters in the object.
  #
  # @param [Hash] values
  #   The names and values to set the instance parameters to.
  #
  # @example
  #   obj.params = {:x => 5, :y => 2}
  #   # => {:x=>5, :y=>2}
  #
  def params=(values)
    values.each do |name,value|
      name = name.to_sym

      if has_param?(name)
        if (value.kind_of?(Parameters::ClassParam) || value.kind_of?(Parameters::InstanceParam))
          value = value.value
        end

        self.params[name].value = value
      end
    end
  end

  #
  # Iterates over each instance parameter in the object.
  #
  # @yield [param]
  #   The block that will be passed each instance parameter.
  #
  def each_param(&block)
    self.params.each_value(&block)
  end

  #
  # @return [Boolean]
  #   Specifies whether or not there is a instance parameter with the
  #   specified name.
  #
  # @example
  #   obj.has_param?('rhost') # => true
  #
  def has_param?(name)
    self.params.has_key?(name.to_sym)
  end

  #
  # Searches for the instance parameter with a specific name.
  #
  # @param [Symbol, String] name
  #   The name of the instance parameter to search for.
  #
  # @return [InstanceParam]
  #   The instance parameter with the specified name.
  #
  # @raise [ParamNotFound]
  #   Could not find the instance parameter with the specified name.
  #
  # @example
  #   obj.get_param('var') # => InstanceParam
  #
  def get_param(name)
    name = name.to_sym

    unless has_param?(name)
      raise(Parameters::ParamNotFound,"parameter #{name.to_s.dump} was not found within #{self.to_s.dump}",caller)
    end

    return self.params[name]
  end

  #
  # Returns the description of the parameter with a specific name.
  #
  # @param [Symbol, String] name
  #   The name of the instance parameter to search for.
  #
  # @return [String]
  #   The description of the instance parameter.
  #
  # @raise [ParamNotFound]
  #   Could not find the instance parameter with the specified name.
  #
  # @example
  #   obj.describe_param('rhost') # => "remote host"
  #
  def describe_param(name)
    get_param(name).description
  end

  #
  # Returns the value of the parameter with a specific name.
  #
  # @param [Symbol, String] name
  #   The name of the instance parameter to search for.
  #
  # @return [Object]
  #   The value of the instance parameter with the specified name.
  #
  # @raise [ParamNotFound]
  #   Could not find the instance parameter with the specified name.
  #
  # @example
  #   obj.param_value('rhost') # => 80
  #
  def param_value(name)
    get_param(name).value
  end

  protected

  #
  # Requires that the instance parameters with specific names have
  # non +nil+ values.
  #
  # @return [true]
  #   All the instance parameters have non +nil+ values.
  #
  # @raise [MissingParam]
  #   One of the instance parameters was not set.
  #
  def require_params(*names)
    names.each do |name|
      name = name.to_s

      if instance_variable_get("@#{name}".to_sym).nil?
        raise(Parameters::MissingParam,"parameter #{name.dump} has no value",caller)
      end
    end

    return true
  end
end