require 'puppet/transportable'

class Puppet::Type
    # Make 'new' private, so people have to use create instead.
    class << self
        private :new
    end

    # retrieve a named instance of the current type
    def self.[](name)
        @objects[name] || @aliases[name]
    end

    # add an instance by name to the class list of instances
    def self.[]=(name,object)
        newobj = nil
        if object.is_a?(Puppet::Type)
            newobj = object
        else
            raise Puppet::DevError, "must pass a Puppet::Type object"
        end

        if exobj = @objects[name] and self.isomorphic?
            msg = "Object '%s[%s]' already exists" %
                [newobj.class.name, name]

            if exobj.file and exobj.line
                msg += ("in file %s at line %s" %
                    [object.file, object.line])
            end
            if object.file and object.line
                msg += ("and cannot be redefined in file %s at line %s" %
                    [object.file, object.line])
            end
            error = Puppet::Error.new(msg)
            raise error
        else
            #Puppet.info("adding %s of type %s to class list" %
            #    [name,object.class])
            @objects[name] = newobj
        end
    end

    # Create an alias.  We keep these in a separate hash so that we don't encounter
    # the objects multiple times when iterating over them.
    def self.alias(name, obj)
        if @objects.include?(name)
            unless @objects[name] == obj
                raise Puppet::Error.new(
                    "Cannot create alias %s: object already exists" %
                    [name]
                )
            end
        end

        if @aliases.include?(name)
            unless @aliases[name] == obj
                raise Puppet::Error.new(
                    "Object %s already has alias %s" %
                    [@aliases[name].name, name]
                )
            end
        end

        @aliases[name] = obj
    end

    # remove all of the instances of a single type
    def self.clear
        if defined? @objects
            @objects.each do |name, obj|
                obj.remove(true)
            end
            @objects.clear
        end
        if defined? @aliases
            @aliases.clear
        end
    end

    # Force users to call this, so that we can merge objects if
    # necessary.
    def self.create(args)
        # Don't modify the original hash; instead, create a duplicate and modify it.
        # We have to dup and use the ! so that it stays a TransObject if it is
        # one.
        hash = args.dup
        symbolizehash!(hash)

        # If we're the base class, then pass the info on appropriately
        if self == Puppet::Type
            type = nil
            if hash.is_a? Puppet::TransObject
                type = hash.type
            else
                # If we're using the type to determine object type, then delete it
                if type = hash[:type]
                    hash.delete(:type)
                end
            end

            # If they've specified a type and called on the base, then
            # delegate to the subclass.
            if type
                if typeklass = self.type(type)
                    return typeklass.create(hash)
                else
                    raise Puppet::Error, "Unknown type %s" % type
                end
            else
                raise Puppet::Error, "No type found for %s" % hash.inspect
            end
        end

        # Handle this new object being implicit
        implicit = hash[:implicit] || false
        if hash.include?(:implicit)
            hash.delete(:implicit)
        end

        name = nil
        unless hash.is_a? Puppet::TransObject
            hash = self.hash2trans(hash)
        end

        # XXX This will have to change when transobjects change to using titles
        title = hash.name

        # if the object already exists
        if self.isomorphic? and retobj = self[title]
            # if only one of our objects is implicit, then it's easy to see
            # who wins -- the non-implicit one.
            if retobj.implicit? and ! implicit
                Puppet.notice "Removing implicit %s" % retobj.title
                # Remove all of the objects, but do not remove their subscriptions.
                retobj.remove(false)

                # now pass through and create the new object
            elsif implicit
                Puppet.debug "Ignoring implicit %s[%s]" % [self.name, title]
                return nil
            else
                raise Puppet::Error, "%s is already being managed" % retobj.ref
            end
        end

        # create it anew
        # if there's a failure, destroy the object if it got that far, but raise
        # the error.
        begin
            obj = new(hash)
        rescue => detail
            Puppet.err "Could not create %s: %s" % [title, detail.to_s]
            if obj
                obj.remove(true)
            elsif obj = self[title]
                obj.remove(true)
            end
            raise
        end

        if implicit
            obj.implicit = true
        end

        # Store the object by title
        self[obj.title] = obj

        return obj
    end

    # remove a specified object
    def self.delete(resource)
        return unless defined? @objects
        if @objects.include?(resource.title)
            @objects.delete(resource.title)
        end
        if @aliases.include?(resource.title)
            @aliases.delete(resource.title)
        end
        if @aliases.has_value?(resource)
            names = []
            @aliases.each do |name, otherres|
                if otherres == resource
                    names << name
                end
            end
            names.each { |name| @aliases.delete(name) }
        end
    end

    # iterate across each of the type's instances
    def self.each
        return unless defined? @objects
        @objects.each { |name,instance|
            yield instance
        }
    end

    # does the type have an object with the given name?
    def self.has_key?(name)
        return @objects.has_key?(name)
    end

    # Convert a hash to a TransObject.
    def self.hash2trans(hash)
        title = nil
        if hash.include? :title
            title = hash[:title]
            hash.delete(:title)
        elsif hash.include? self.namevar
            title = hash[self.namevar]
            hash.delete(self.namevar)

            if hash.include? :name
                raise ArgumentError, "Cannot provide both name and %s to %s" %
                    [self.namevar, self.name]
            end
        elsif hash[:name]
            title = hash[:name]
            hash.delete :name
        end

        if catalog = hash[:catalog]
            hash.delete(:catalog)
        end

        raise(Puppet::Error, "You must specify a title for objects of type %s" % self.to_s) unless title

        if hash.include? :type
            unless self.validattr? :type
                hash.delete :type
            end
        end

        # okay, now make a transobject out of hash
        begin
            trans = Puppet::TransObject.new(title, self.name.to_s)
            trans.catalog = catalog if catalog
            hash.each { |param, value|
                trans[param] = value
            }
        rescue => detail
            raise Puppet::Error, "Could not create %s: %s" %
                [name, detail]
        end

        return trans
    end

    # Retrieve all known instances.  Either requires providers or must be overridden.
    def self.instances
        unless defined?(@providers) and ! @providers.empty?
            raise Puppet::DevError, "%s has no providers and has not overridden 'instances'" % self.name
        end

        # Put the default provider first, then the rest of the suitable providers.
        provider_instances = {}
        providers_by_source.collect do |provider|
            provider.instances.collect do |instance|
                # First try to get the resource if it already exists
                # Skip instances that map to a managed resource with a different provider
                next if resource = self[instance.name] and resource.provider.class != instance.class

                # We always want to use the "first" provider instance we find, unless the resource
                # is already managed and has a different provider set
                if other = provider_instances[instance.name]
                    Puppet.warning "%s %s found in both %s and %s; skipping the %s version" %
                        [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name]
                    next
                end
                provider_instances[instance.name] = instance

                if resource
                    resource.provider = instance
                    resource
                else
                    create(:name => instance.name, :provider => instance, :check => :all)
                end
            end
        end.flatten.compact
    end

    # Return a list of one suitable provider per source, with the default provider first.
    def self.providers_by_source
        # Put the default provider first, then the rest of the suitable providers.
        sources = []
        [defaultprovider, suitableprovider].flatten.uniq.collect do |provider|
            next if sources.include?(provider.source)

            sources << provider.source
            provider
        end.compact
    end

    # Create the path for logging and such.
    def pathbuilder
        if p = parent
            [p.pathbuilder, self.ref].flatten
        else
            [self.ref]
        end
    end
end

