require 'hiera/util'

class Hiera
  module Backend
    class << self
      # Data lives in /var/lib/hiera by default.  If a backend
      # supplies a datadir in the config it will be used and
      # subject to variable expansion based on scope
      def datadir(backend, scope)
        backend = backend.to_sym
        default = Hiera::Util.var_dir

        if Config.include?(backend)
          parse_string(Config[backend][:datadir] || default, scope)
        else
          parse_string(default, scope)
        end
      end

      # Finds the path to a datafile based on the Backend#datadir
      # and extension
      #
      # If the file is not found nil is returned
      def datafile(backend, scope, source, extension)
        file = File.join([datadir(backend, scope), "#{source}.#{extension}"])

        unless File.exist?(file)
          Hiera.debug("Cannot find datafile #{file}, skipping")

          return nil
        end

        return file
      end

      # Constructs a list of data sources to search
      #
      # If you give it a specific hierarchy it will just use that
      # else it will use the global configured one, failing that
      # it will just look in the 'common' data source.
      #
      # An override can be supplied that will be pre-pended to the
      # hierarchy.
      #
      # The source names will be subject to variable expansion based
      # on scope
      def datasources(scope, override=nil, hierarchy=nil)
        if hierarchy
          hierarchy = [hierarchy]
        elsif Config.include?(:hierarchy)
          hierarchy = [Config[:hierarchy]].flatten
        else
          hierarchy = ["common"]
        end

        hierarchy.insert(0, override) if override

        hierarchy.flatten.map do |source|
          source = parse_string(source, scope)
          yield(source) unless source == "" or source =~ /(^\/|\/\/|\/$)/
        end
      end

      # Parse a string like '%{foo}' against a supplied
      # scope and additional scope.  If either scope or
      # extra_scope includes the varaible 'foo' it will
      # be replaced else an empty string will be placed.
      #
      # If both scope and extra_data has "foo" scope
      # will win.  See hiera-puppet for an example of
      # this to make hiera aware of additional non scope
      # variables
      def parse_string(data, scope, extra_data={})
        return nil unless data

        tdata = data.clone

        if tdata.is_a?(String)
          while tdata =~ /%\{(.+?)\}/
            begin
              var = $1

              val = ""

              # Puppet can return :undefined for unknown scope vars,
              # If it does then we still need to evaluate extra_data
              # before returning an empty string.
              if scope[var] && scope[var] != :undefined
                  val = scope[var]
              elsif extra_data[var]
                  val = extra_data[var]
              end
            end until val != "" || var !~ /::(.+)/

            tdata.gsub!(/%\{(::)?#{var}\}/, val)
          end
        end

        return tdata
      end

      # Parses a answer received from data files
      #
      # Ultimately it just pass the data through parse_string but
      # it makes some effort to handle arrays of strings as well
      def parse_answer(data, scope, extra_data={})
        if data.is_a?(Numeric) or data.is_a?(TrueClass) or data.is_a?(FalseClass)
          return data
        elsif data.is_a?(String)
          return parse_string(data, scope, extra_data)
        elsif data.is_a?(Hash)
          answer = {}
          data.each_pair do |key, val|
            answer[key] = parse_answer(val, scope, extra_data)
          end

          return answer
        elsif data.is_a?(Array)
          answer = []
          data.each do |item|
            answer << parse_answer(item, scope, extra_data)
          end

          return answer
        end
      end

      def resolve_answer(answer, resolution_type)
        case resolution_type
        when :array
          [answer].flatten.uniq.compact
        when :hash
          answer # Hash structure should be preserved
        else
          answer
        end
      end

      # Calls out to all configured backends in the order they
      # were specified.  The first one to answer will win.
      #
      # This lets you declare multiple backends, a possible
      # use case might be in Puppet where a Puppet module declares
      # default data using in-module data while users can override
      # using JSON/YAML etc.  By layering the backends and putting
      # the Puppet one last you can override module author data
      # easily.
      #
      # Backend instances are cached so if you need to connect to any
      # databases then do so in your constructor, future calls to your
      # backend will not create new instances
      def lookup(key, default, scope, order_override, resolution_type)
        @backends ||= {}
        answer = nil

        Config[:backends].each do |backend|
          if constants.include?("#{backend.capitalize}_backend") || constants.include?("#{backend.capitalize}_backend".to_sym)
            @backends[backend] ||= Backend.const_get("#{backend.capitalize}_backend").new
            new_answer = @backends[backend].lookup(key, scope, order_override, resolution_type)

            if not new_answer.nil?
              case resolution_type
              when :array
                raise Exception, "Hiera type mismatch: expected Array and got #{new_answer.class}" unless new_answer.kind_of? Array or new_answer.kind_of? String
                answer ||= []
                answer << new_answer
              when :hash
                raise Exception, "Hiera type mismatch: expected Hash and got #{new_answer.class}" unless new_answer.kind_of? Hash
                answer ||= {}
                answer = new_answer.merge answer
              else
                answer = new_answer
                break
              end
            end
          end
        end

        answer = resolve_answer(answer, resolution_type) unless answer.nil?
        answer = parse_string(default, scope) if answer.nil? and default.is_a?(String)

        return default if answer.nil?
        return answer
      end
    end
  end
end
