require 'find'
require 'forwardable'
require 'puppet/node/environment'

class Puppet::Parser::TypeLoader
  extend  Forwardable
  include Puppet::Node::Environment::Helper

  # Helper class that makes sure we don't try to import the same file
  # more than once from either the same thread or different threads.
  class Helper
    include MonitorMixin
    def initialize
      super
      # These hashes are indexed by filename
      @state = {} # :doing or :done
      @thread = {} # if :doing, thread that's doing the parsing
      @cond_var = {} # if :doing, condition var that will be signaled when done.
    end

    # Execute the supplied block exactly once per file, no matter how
    # many threads have asked for it to run.  If another thread is
    # already executing it, wait for it to finish.  If this thread is
    # already executing it, return immediately without executing the
    # block.
    #
    # Note: the reason for returning immediately if this thread is
    # already executing the block is to handle the case of a circular
    # import--when this happens, we attempt to recursively re-parse a
    # file that we are already in the process of parsing.  To prevent
    # an infinite regress we need to simply do nothing when the
    # recursive import is attempted.
    def do_once(file)
      need_to_execute = synchronize do
        case @state[file]
        when :doing
          if @thread[file] != Thread.current
            @cond_var[file].wait
          end
          false
        when :done
          false
        else
          @state[file] = :doing
          @thread[file] = Thread.current
          @cond_var[file] = new_cond
          true
        end
      end
      if need_to_execute
        begin
          yield
        ensure
          synchronize do
            @state[file] = :done
            @thread.delete(file)
            @cond_var.delete(file).broadcast
          end
        end
      end
    end
  end

  # Import our files.
  def import(file, current_file = nil)
    return if Puppet[:ignoreimport]

    # use a path relative to the file doing the importing
    if current_file
      dir = current_file.sub(%r{[^/]+$},'').sub(/\/$/, '')
    else
      dir = "."
    end
    if dir == ""
      dir = "."
    end

    pat = file
    modname, files = Puppet::Parser::Files.find_manifests(pat, :cwd => dir, :environment => environment)
    if files.size == 0
      raise Puppet::ImportError.new("No file(s) found for import of '#{pat}'")
    end

    loaded_asts = []
    files.each do |file|
      unless Puppet::Util.absolute_path?(file)
        file = File.join(dir, file)
      end
      @loading_helper.do_once(file) do
        loaded_asts << parse_file(file)
      end
    end
    loaded_asts.inject([]) do |loaded_types, ast|
      loaded_types + known_resource_types.import_ast(ast, modname)
    end
  end

  def import_all
    # And then load all files from each module, but (relying on system
    # behavior) only load files from the first module of a given name.  E.g.,
    # given first/foo and second/foo, only files from first/foo will be loaded.
    environment.modules.each do |mod|
      Find.find(mod.manifests) do |path|
        if path =~ /\.pp$/ or path =~ /\.rb$/
          import(path)
        end
      end
    end
  end

  def_delegator :environment, :known_resource_types

  def initialize(env)
    self.environment = env
    @loading_helper = Helper.new
  end

  # Try to load the object with the given fully qualified name.
  def try_load_fqname(type, fqname)
    return nil if fqname == "" # special-case main.
    name2files(fqname).each do |filename|
      begin
        imported_types = import(filename)
        if result = imported_types.find { |t| t.type == type and t.name == fqname }
          Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}"
          return result
        end
      rescue Puppet::ImportError => detail
        # We couldn't load the item
        # I'm not convienced we should just drop these errors, but this
        # preserves existing behaviours.
      end
    end
    # Nothing found.
    return nil
  end

  def parse_file(file)
    Puppet.debug("importing '#{file}' in environment #{environment}")
    parser = Puppet::Parser::Parser.new(environment)
    parser.file = file
    return parser.parse
  end

  private

  # Return a list of all file basenames that should be tried in order
  # to load the object with the given fully qualified name.
  def name2files(fqname)
    result = []
    ary = fqname.split("::")
    while ary.length > 0
      result << ary.join(File::SEPARATOR)
      ary.pop
    end
    return result
  end
end
