require 'puppet/util/methodhelper'
require 'puppet/util/log_paths'
require 'puppet/util/logging'
require 'puppet/util/docs'

class Puppet::Parameter
    include Puppet::Util
    include Puppet::Util::Errors
    include Puppet::Util::LogPaths
    include Puppet::Util::Logging
    include Puppet::Util::MethodHelper
    class << self
        include Puppet::Util
        include Puppet::Util::Docs
        attr_reader :validater, :munger, :name, :default, :required_features
        attr_accessor :metaparam

        # Define the default value for a given parameter or parameter.  This
        # means that 'nil' is an invalid default value.  This defines
        # the 'default' instance method.
        def defaultto(value = nil, &block)
            if block
                define_method(:default, &block)
            else
                if value.nil?
                    raise Puppet::DevError,
                        "Either a default value or block must be provided"
                end
                define_method(:default) do value end
            end
        end

        # Return a documentation string.  If there are valid values,
        # then tack them onto the string.
        def doc
            @doc ||= ""

            unless defined? @addeddocvals
                unless values.empty?
                    if @aliasvalues.empty?
                        @doc += "  Valid values are ``" +
                            values.join("``, ``") + "``."
                    else
                        @doc += "  Valid values are "

                        @doc += values.collect do |value|
                            ary = @aliasvalues.find do |name, val|
                                val == value
                            end
                            if ary
                                "``%s`` (also called ``%s``)" % [value, ary[0]]
                            else
                                "``#{value}``"
                            end
                        end.join(", ") + "."
                    end
                end

                if defined? @parameterregexes and ! @parameterregexes.empty?
                    regs = @parameterregexes
                    if @parameterregexes.is_a? Hash
                        regs = @parameterregexes.keys
                    end
                    unless regs.empty?
                        @doc += "  Values can also match ``" +
                            regs.join("``, ``") + "``."
                    end
                end

                if f = self.required_features
                    @doc += "  Requires features %s." % f.flatten.collect { |f| f.to_s }.join(" ")
                end
                @addeddocvals = true
            end

            @doc
        end

        def nodefault
            if public_method_defined? :default
                undef_method :default
            end
        end

        # Store documentation for this parameter.
        def desc(str)
            @doc = str
        end

        def initvars
            @parametervalues = []
            @aliasvalues = {}
            @parameterregexes = []
        end

        # This is how we munge the value.  Basically, this is our
        # opportunity to convert the value from one form into another.
        def munge(&block)
            # I need to wrap the unsafe version in begin/rescue parameterments,
            # but if I directly call the block then it gets bound to the
            # class's context, not the instance's, thus the two methods,
            # instead of just one.
            define_method(:unsafe_munge, &block)

            define_method(:munge) do |*args|
                begin
                    ret = unsafe_munge(*args)
                rescue Puppet::Error => detail
                    Puppet.debug "Reraising %s" % detail
                    raise
                rescue => detail
                    raise Puppet::DevError, "Munging failed for value %s in class %s: %s" %
                        [args.inspect, self.name, detail], detail.backtrace
                end

                if self.shadow
                    self.shadow.munge(*args)
                end
                ret
            end
        end

        # Mark whether we're the namevar.
        def isnamevar
            @isnamevar = true
            @required = true
        end

        # Is this parameter the namevar?  Defaults to false.
        def isnamevar?
            if defined? @isnamevar
                return @isnamevar
            else
                return false
            end
        end

        # This parameter is required.
        def isrequired
            @required = true
        end

        # Is this parameter required?  Defaults to false.
        def required?
            if defined? @required
                return @required
            else
                return false
            end
        end

        # Verify that we got a good value
        def validate(&block)
            #@validater = block
            define_method(:unsafe_validate, &block)

            define_method(:validate) do |*args|
                begin
                    unsafe_validate(*args)
                rescue ArgumentError, Puppet::Error, TypeError
                    raise
                rescue => detail
                    raise Puppet::DevError,
                        "Validate method failed for class %s: %s" %
                        [self.name, detail], detail.backtrace
                end
            end
        end

        # Does the value match any of our regexes?
        def match?(value)
            value = value.to_s unless value.is_a? String
            @parameterregexes.find { |r|
                r = r[0] if r.is_a? Array # Properties use a hash here
                r =~ value
            }
        end

        # Define a new value for our parameter.
        def newvalues(*names)
            names.each { |name|
                name = name.intern if name.is_a? String

                case name
                when Symbol
                    if @parametervalues.include?(name)
                        Puppet.warning "%s already has a value for %s" %
                            [name, name]
                    end
                    @parametervalues << name
                when Regexp
                    if @parameterregexes.include?(name)
                        Puppet.warning "%s already has a value for %s" %
                            [name, name]
                    end
                    @parameterregexes << name
                else
                    raise ArgumentError, "Invalid value %s of type %s" %
                        [name, name.class]
                end
            }
        end

        def aliasvalue(name, other)
            other = symbolize(other)
            unless @parametervalues.include?(other)
                raise Puppet::DevError,
                    "Cannot alias nonexistent value %s" % other
            end

            @aliasvalues[name] = other
        end

        def alias(name)
            @aliasvalues[name]
        end

        def regexes
            return @parameterregexes.dup
        end

        def required_features=(*args)
            @required_features = args.flatten.collect { |a| a.to_s.downcase.intern }
        end

        # Return the list of valid values.
        def values
            #[@aliasvalues.keys, @parametervalues.keys].flatten
            if @parametervalues.is_a? Array
                return @parametervalues.dup
            elsif @parametervalues.is_a? Hash
                return @parametervalues.keys
            else
                return []
            end
        end
    end

    # Just a simple method to proxy instance methods to class methods
    def self.proxymethods(*values)
        values.each { |val|
            define_method(val) do
                self.class.send(val)
            end
        }
    end

    # And then define one of these proxies for each method in our
    # ParamHandler class.
    proxymethods("required?", "isnamevar?")

    attr_accessor :resource
    # LAK 2007-05-09: Keep the @parent around for backward compatibility.
    attr_accessor :parent
    attr_reader :shadow

    def devfail(msg)
        self.fail(Puppet::DevError, msg)
    end

    def fail(*args)
        type = nil
        if args[0].is_a?(Class)
            type = args.shift
        else
            type = Puppet::Error
        end

        error = type.new(args.join(" "))

        if defined? @resource and @resource and @resource.line
            error.line = @resource.line
        end

        if defined? @resource and @resource and @resource.file
            error.file = @resource.file
        end

        raise error
    end

    # Basic parameter initialization.
    def initialize(options = {})
        options = symbolize_options(options)
        if resource = options[:resource]
            self.resource = resource
            options.delete(:resource)
        else
            raise Puppet::DevError, "No resource set for %s" % self.class.name
        end

        if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name)
            setup_shadow(klass)
        end

        set_options(options)
    end

    # Log a message using the resource's log level.
    def log(msg)
        unless @resource[:loglevel]
            p @resource
            self.devfail "Parent %s has no loglevel" %
                @resource.name
        end
        Puppet::Util::Log.create(
            :level => @resource[:loglevel],
            :message => msg,
            :source => self
        )
    end

    # Is this parameter a metaparam?
    def metaparam?
        self.class.metaparam
    end

    # each parameter class must define the name() method, and parameter
    # instances do not change that name this implicitly means that a given
    # object can only have one parameter instance of a given parameter
    # class
    def name
        return self.class.name
    end

    # for testing whether we should actually do anything
    def noop
        unless defined? @noop
            @noop = false
        end
        tmp = @noop || self.resource.noop || Puppet[:noop] || false
        #debug "noop is %s" % tmp
        return tmp
    end

    # return the full path to us, for logging and rollback; not currently
    # used
    def pathbuilder
        if defined? @resource and @resource
            return [@resource.pathbuilder, self.name]
        else
            return [self.name]
        end
    end

    # If the specified value is allowed, then munge appropriately.
    munge do |value|
        if self.class.values.empty? and self.class.regexes.empty?
            # This parameter isn't using defined values to do its work.
            return value
        end

        # We convert to a string and then a symbol so that things like
        # booleans work as we expect.
        intern = value.to_s.intern

        # If it's a valid value, always return it as a symbol.
        if self.class.values.include?(intern)
            retval = intern
        elsif other = self.class.alias(intern)
            retval = other
        elsif ary = self.class.match?(value)
            retval = value
        else
            # If it passed the validation but is not a registered value,
            # we just return it as is.
            retval = value
        end

        retval
    end

    # Verify that the passed value is valid.
    validate do |value|
        vals = self.class.values
        regs = self.class.regexes

        # this is true on properties
        regs = regs.keys if regs.is_a?(Hash)

        # This parameter isn't using defined values to do its work.
        return if vals.empty? and regs.empty?

        newval = value
        newval = value.to_s.intern unless value.is_a?(Symbol)

        name = newval

        unless vals.include?(newval) or name = self.class.alias(newval) or name = self.class.match?(value) # We match the string, not the symbol
            str = "Invalid '%s' value %s. " %
                [self.class.name, value.inspect]

            unless vals.empty?
                str += "Valid values are %s. " % vals.join(", ")
            end

            unless regs.empty?
                str += "Valid values match %s." % regs.collect { |r|
                    r.to_s
                }.join(", ")
            end

            raise ArgumentError, str
        end

        # Now check for features.
        name = name[0] if name.is_a?(Array) # This is true for regexes.
        validate_features_per_value(name) if is_a?(Puppet::Property)
    end

    def remove
        @resource = nil
        @shadow = nil
    end

    # This should only be called for parameters, but go ahead and make
    # it possible to call for properties, too.
    def value
        if self.is_a?(Puppet::Property)
            # We should return the 'is' value if there's not 'should'
            # value.  This might be bad, though, because the 'should'
            # method knows whether to return an array or not and that info
            # is not exposed, and the 'is' value could be a symbol.  I
            # can't seem to create a test in which this is a problem, but
            # that doesn't mean it's not one.
            if self.should
                return self.should
            else
                return self.retrieve
            end
        else
            if defined? @value
                return @value
            else
                return nil
            end
        end
    end

    # Store the value provided.  All of the checking should possibly be
    # late-binding (e.g., users might not exist when the value is assigned
    # but might when it is asked for).
    def value=(value)
        # If we're a parameter, just hand the processing off to the should
        # method.
        if self.is_a?(Puppet::Property)
            return self.should = value
        end
        if respond_to?(:validate)
            validate(value)
        end

        if respond_to?(:munge)
            value = munge(value)
        end
        @value = value
    end

    def inspect
        s = "Parameter(%s = %s" % [self.name, self.value || "nil"]
        if defined? @resource
            s += ", @resource = %s)" % @resource
        else
            s += ")"
        end
    end

    # Retrieve the resource's provider.  Some types don't have providers, in which
    # case we return the resource object itself.
    def provider
        @resource.provider || @resource
    end

    # If there's a shadowing metaparam, instantiate it now.
    # This allows us to create a property or parameter with the
    # same name as a metaparameter, and the metaparam will only be
    # stored as a shadow.
    def setup_shadow(klass)
        @shadow = klass.new(:resource => self.resource)
    end

    def to_s
        s = "Parameter(%s)" % self.name
    end

    # Make sure that we've got all of the required features for a given value.
    def validate_features_per_value(value)
        if features = self.class.value_option(value, :required_features)
            raise ArgumentError, "Provider must have features '%s' to set '%s' to '%s'" % [features, self.class.name, value] unless provider.satisfies?(features)
        end
    end
end

