require "pathname"

require "log4r"

module Vagrant
  module Config
    # This class is responsible for loading Vagrant configuration,
    # usually in the form of Vagrantfiles.
    #
    # Loading works by specifying the sources for the configuration
    # as well as the order the sources should be loaded. Configuration
    # set later always overrides those set earlier; this is how
    # configuration "scoping" is implemented.
    class Loader
      # This is an array of symbols specifying the order in which
      # configuration is loaded. For examples, see the class documentation.
      attr_accessor :load_order

      def initialize
        @logger  = Log4r::Logger.new("vagrant::config::loader")
        @sources = {}
        @proc_cache = {}
        @config_cache = {}
      end

      # Set the configuration data for the given name.
      #
      # The `name` should be a symbol and must uniquely identify the data
      # being given.
      #
      # `data` can either be a path to a Ruby Vagrantfile or a `Proc` directly.
      # `data` can also be an array of such values.
      #
      # At this point, no configuration is actually loaded. Note that calling
      # `set` multiple times with the same name will override any previously
      # set values. In this way, the last set data for a given name wins.
      def set(name, sources)
        @logger.debug("Set #{name.inspect} = #{sources.inspect}")

        # Sources should be an array
        sources = [sources] if !sources.kind_of?(Array)

        # Gather the procs for every source, since that is what we care about.
        procs = []
        sources.each do |source|
          if !@proc_cache.has_key?(source)
            # Load the procs for this source and cache them. This caching
            # avoids the issue where a file may have side effects when loading
            # and loading it multiple times causes unexpected behavior.
            @logger.debug("Populating proc cache for #{source.inspect}")
            @proc_cache[source] = procs_for_source(source)
          end

          # Add on to the array of procs we're going to use
          procs.concat(@proc_cache[source])
        end

        # Set this source by name.
        @sources[name] = procs
      end

      # This loads the configured sources in the configured order and returns
      # an actual configuration object that is ready to be used.
      def load
        @logger.debug("Loading configuration in order: #{@load_order.inspect}")

        unknown_sources = @sources.keys - @load_order
        if !unknown_sources.empty?
          # TODO: Raise exception here perhaps.
          @logger.error("Unknown config sources: #{unknown_sources.inspect}")
        end

        # Create the top-level configuration which will hold all the config.
        result = Top.new

        @load_order.each do |key|
          next if !@sources.has_key?(key)

          @sources[key].each do |proc|
            if !@config_cache.has_key?(proc)
              @logger.debug("Loading from: #{key} (evaluating)")
              current = Top.new
              proc.call(current)
              @config_cache[proc] = current
            else
              @logger.debug("Loading from: #{key} (cache)")
            end

            # Merge in the results of this proc's configuration
            result = result.merge(@config_cache[proc])
          end
        end

        @logger.debug("Configuration loaded successfully")
        result
      end

      protected

      # This returns an array of `Proc` objects for the given source.
      # The `Proc` objects returned will expect a single argument for
      # the configuration object and are expected to mutate this
      # configuration object.
      def procs_for_source(source)
        return [source] if source.is_a?(Proc)

        # Assume all string sources are actually pathnames
        source = Pathname.new(source) if source.is_a?(String)

        if source.is_a?(Pathname)
          @logger.debug("Load procs for pathname: #{source.inspect}")

          begin
            return Config.capture_configures do
              Kernel.load source
            end
          rescue SyntaxError => e
            # Report syntax errors in a nice way.
            raise Errors::VagrantfileSyntaxError, :file => e.message
          end
        end

        raise Exception, "Unknown configuration source: #{source.inspect}"
      end
    end
  end
end
