require 'puppet'
require 'yaml'

module Puppet
  # The transportable objects themselves.  Basically just a hash with some
  # metadata and a few extra methods.  I used to have the object actually
  # be a subclass of Hash, but I could never correctly dump them using
  # YAML.
  class TransObject
    include Enumerable
    attr_accessor :type, :name, :file, :line, :catalog

    attr_writer :tags

    %w{has_key? include? length delete empty? << [] []=}.each { |method|
      define_method(method) do |*args|
        @params.send(method, *args)
      end
    }

    def each
      @params.each { |p,v| yield p, v }
    end

    def initialize(name,type)
      @type = type.to_s.downcase
      @name = name
      @params = {}
      @tags = []
    end

    def longname
      [@type,@name].join('--')
    end

    def ref
      @ref ||= Puppet::Resource.new(@type, @name)
      @ref.to_s
    end

    def tags
      @tags
    end

    # Convert a defined type into a component.
    def to_component
      trans = TransObject.new(ref, :component)
      @params.each { |param,value|
        next unless Puppet::Type::Component.valid_parameter?(param)
        Puppet.debug "Defining #{param} on #{ref}"
        trans[param] = value
      }
      trans.catalog = self.catalog
      Puppet::Type::Component.create(trans)
    end

    def to_hash
      @params.dup
    end

    def to_s
      "#{@type}(#{@name}) => #{super}"
    end

    def to_manifest
      "%s { '%s':\n%s\n}" % [self.type.to_s, self.name,
        @params.collect { |p, v|
          if v.is_a? Array
            "    #{p} => [\'#{v.join("','")}\']"
          else
            "    #{p} => \'#{v}\'"
          end
        }.join(",\n")
        ]
    end

    # Create a normalized resource from our TransObject.
    def to_resource
      result = Puppet::Resource.new(type, name, :parameters => @params.dup)
      result.tag(*tags)

      result
    end

    def to_yaml_properties
      instance_variables.reject { |v| %w{@ref}.include?(v) }
    end

    def to_ref
      ref
    end

    def to_ral
      to_resource.to_ral
    end
  end

  # Just a linear container for objects.  Behaves mostly like an array, except
  # that YAML will correctly dump them even with their instance variables.
  class TransBucket
    include Enumerable

    attr_accessor :name, :type, :file, :line, :classes, :keyword, :top, :catalog

    %w{delete shift include? length empty? << []}.each { |method|
      define_method(method) do |*args|
        #Puppet.warning "Calling #{method} with #{args.inspect}"
        @children.send(method, *args)
        #Puppet.warning @params.inspect
      end
    }

    # Recursively yield everything.
    def delve(&block)
      @children.each do |obj|
        block.call(obj)
        if obj.is_a? self.class
          obj.delve(&block)
        else
          obj
        end
      end
    end

    def each
      @children.each { |c| yield c }
    end

    # Turn our heirarchy into a flat list
    def flatten
      @children.collect do |obj|
        if obj.is_a? Puppet::TransBucket
          obj.flatten
        else
          obj
        end
      end.flatten
    end

    def initialize(children = [])
      @children = children
    end

    def push(*args)
      args.each { |arg|
        case arg
        when Puppet::TransBucket, Puppet::TransObject
          # nada
        else
          raise Puppet::DevError,
            "TransBuckets cannot handle objects of type #{arg.class}"
        end
      }
      @children += args
    end

    # Convert to a parseable manifest
    def to_manifest
      unless self.top
        raise Puppet::DevError, "No keyword; cannot convert to manifest" unless @keyword
      end

      str = "#{@keyword} #{@name} {\n%s\n}"
      str % @children.collect { |child|
        child.to_manifest
      }.collect { |str|
        if self.top
          str
        else
          str.gsub(/^/, "    ") # indent everything once
        end
      }.join("\n\n") # and throw in a blank line
    end

    def to_yaml_properties
      instance_variables
    end

    # Create a resource graph from our structure.
    def to_catalog(clear_on_failure = true)
      catalog = Puppet::Resource::Catalog.new(Facter.value("hostname"))

      # This should really use the 'delve' method, but this
      # whole class is going away relatively soon, hopefully,
      # so it's not worth it.
      delver = proc do |obj|
        obj.catalog = catalog
        unless container = catalog.resource(obj.to_ref)
          container = obj.to_ral
          catalog.add_resource container
        end
        obj.each do |child|
          child.catalog = catalog
          unless resource = catalog.resource(child.to_ref)
            resource = child.to_ral
            catalog.add_resource resource
          end

          catalog.add_edge(container, resource)
          delver.call(child) if child.is_a?(self.class)
        end
      end

      begin
        delver.call(self)
        catalog.finalize
      rescue => detail
        # This is important until we lose the global resource references.
        catalog.clear if (clear_on_failure)
        raise
      end

      catalog
    end

    def to_ref
      unless defined?(@ref)
        if self.type and self.name
          @ref = Puppet::Resource.new(self.type, self.name)
        elsif self.type and ! self.name # This is old-school node types
          @ref = Puppet::Resource.new("node", self.type)
        elsif ! self.type and self.name
          @ref = Puppet::Resource.new("component", self.name)
        else
          @ref = nil
        end
      end
      @ref.to_s if @ref
    end

    def to_ral
      to_resource.to_ral
    end

    # Create a normalized resource from our TransObject.
    def to_resource
      params = defined?(@parameters) ? @parameters.dup : {}
      Puppet::Resource.new(type, name, :parameters => params)
    end

    def param(param,value)
      @parameters ||= {}
      @parameters[param] = value
    end

  end
end

