require 'puppet/parser/type_loader'
require 'puppet/util/file_watcher'
require 'puppet/util/warnings'

# @api private
class Puppet::Resource::TypeCollection
  attr_reader :environment
  attr_accessor :parse_failed

  include Puppet::Util::Warnings

  def clear
    @hostclasses.clear
    @definitions.clear
    @applications.clear
    @nodes.clear
    @notfound.clear
    @capability_mappings.clear
    @sites.clear
  end

  def initialize(env)
    @environment = env
    @hostclasses = {}
    @definitions = {}
    @capability_mappings = {}
    @applications = {}
    @nodes = {}
    @notfound = {}
    @sites = []

    # So we can keep a list and match the first-defined regex
    @node_list = []
  end

  def import_ast(ast, modname)
    ast.instantiate(modname).each do |instance|
      add(instance)
    end
  end

  def inspect
    "TypeCollection" + {
      :hostclasses => @hostclasses.keys,
      :definitions => @definitions.keys,
      :nodes => @nodes.keys,
      :capability_mappings => @capability_mappings.keys,
      :applications => @applications.keys,
      :site => @sites[0] # todo, could be just a binary, this dumps the entire body (good while developing)
    }.inspect
  end

  # @api private
  def <<(thing)
    add(thing)
    self
  end

  def add(instance)
    # return a merged instance, or the given
    catch(:merged) {
      send("add_#{instance.type}", instance)
      instance.resource_type_collection = self
      instance
    }
  end

  def add_hostclass(instance)
    handle_hostclass_merge(instance)
    dupe_check(instance, @hostclasses) { |dupe| _("Class '%{klass}' is already defined%{error}; cannot redefine") % { klass: instance.name, error: dupe.error_context } }
    dupe_check(instance, @definitions) { |dupe| _("Definition '%{klass}' is already defined%{error}; cannot be redefined as a class") % { klass: instance.name, error: dupe.error_context } }
    dupe_check(instance, @applications) { |dupe| _("Application '%{klass}' is already defined%{error}; cannot be redefined as a class") % { klass: instance.name, error: dupe.error_context } }

    @hostclasses[instance.name] = instance
    instance
  end

  def handle_hostclass_merge(instance)
    # Only main class (named '') can be merged (for purpose of merging top-scopes).
    return instance unless instance.name == ''
    if instance.type == :hostclass && (other = @hostclasses[instance.name]) && other.type == :hostclass
      other.merge(instance)
      # throw is used to signal merge - avoids dupe checks and adding it to hostclasses
      throw :merged, other
    end
  end

  # Replaces the known settings with a new instance (that must be named 'settings').
  # This is primarily needed for testing purposes. Also see PUP-5954 as it makes 
  # it illegal to merge classes other than the '' (main) class. Prior to this change
  # settings where always merged rather than being defined from scratch for many testing scenarios
  # not having a complete side effect free setup for compilation.
  # 
  def replace_settings(instance)
    @hostclasses['settings'] = instance
  end

  def hostclass(name)
    @hostclasses[munge_name(name)]
  end

  def add_node(instance)
    dupe_check(instance, @nodes) { |dupe| _("Node '%{name}' is already defined%{error}; cannot redefine") % { name: instance.name, error: dupe.error_context } }

    @node_list << instance
    @nodes[instance.name] = instance
    instance
  end

  def add_site(instance)
    dupe_check_singleton(instance, @sites) { |dupe| _("Site is already defined%{error}; cannot redefine") % { error: dupe.error_context } }
    @sites << instance
    instance
  end

  def site(_)
    @sites[0]
  end

  def loader
    @loader ||= Puppet::Parser::TypeLoader.new(environment)
  end

  def node(name)
    name = munge_name(name)

    if node = @nodes[name]
      return node
    end

    @node_list.each do |n|
      next unless n.name_is_regex?
      return n if n.match(name)
    end
    nil
  end

  def node_exists?(name)
    @nodes[munge_name(name)]
  end

  def nodes?
    @nodes.length > 0
  end

  def add_definition(instance)
    dupe_check(instance, @hostclasses) { |dupe| _("'%{name}' is already defined%{error} as a class; cannot redefine as a definition") % { name: instance.name, error: dupe.error_context } }
    dupe_check(instance, @definitions) { |dupe| _("Definition '%{name}' is already defined%{error}; cannot be redefined") % { name: instance.name, error: dupe.error_context } }
    dupe_check(instance, @applications) { |dupe| _("'%{name}' is already defined%{error} as an application; cannot be redefined") % { name: instance.name, error: dupe.error_context } }
    @definitions[instance.name] = instance
  end

  def add_capability_mapping(instance)
    dupe_check(instance, @capability_mappings) { |dupe| _("'%{name}' is already defined%{error} as a class; cannot redefine as a mapping") % { name: instance.name, error: dupe.error_context } }
    @capability_mappings[instance.name] = instance
  end

  def definition(name)
    @definitions[munge_name(name)]
  end

  def add_application(instance)
    dupe_check(instance, @hostclasses) { |dupe| _("'%{name}' is already defined%{error} as a class; cannot redefine as an application") % { name: instance.name, error: dupe.error_context } }
    dupe_check(instance, @definitions) { |dupe| _("'%{name}' is already defined%{error} as a definition; cannot redefine as an application") % { name: instance.name, error: dupe.error_context } }
    dupe_check(instance, @applications) { |dupe| _("'%{name}' is already defined%{error} as an application; cannot be redefined") % { name: instance.name, error: dupe.error_context } }
    @applications[instance.name] = instance
  end

  def application(name)
    @applications[munge_name(name)]
  end

  def find_node(name)
    @nodes[munge_name(name)]
  end

  def find_site()
    @sites[0]
  end

  def find_hostclass(name)
    find_or_load(name, :hostclass)
  end

  def find_definition(name)
    find_or_load(name, :definition)
  end

  def find_application(name)
    find_or_load(name, :application)
  end

  # TODO: This implementation is wasteful as it creates a copy on each request
  #
  [:hostclasses, :nodes, :definitions, :capability_mappings,
   :applications].each do |m|
    define_method(m) do
      instance_variable_get("@#{m}").dup
    end
  end

  def parse_failed?
    @parse_failed
  end

  def version
    if !defined?(@version)
      if environment.config_version.nil? || environment.config_version == ""
        @version = Time.now.to_i
      else
        @version = Puppet::Util::Execution.execute([environment.config_version]).to_s.strip
      end
    end

    @version
  rescue Puppet::ExecutionFailure => e
    raise Puppet::ParseError, _("Execution of config_version command `%{cmd}` failed: %{message}") % { cmd: environment.config_version, message: e.message }, e.backtrace
  end

  private

  COLON_COLON = "::".freeze

  # Resolve namespaces and find the given object.  Autoload it if
  # necessary.
  def find_or_load(name, type)
    # Name is always absolute, but may start with :: which must be removed
    fqname = (name[0,2] == COLON_COLON ? name[2..-1] : name)

    result = send(type, fqname)
    unless result
      if @notfound[ fqname ] && Puppet[ :ignoremissingtypes ]
        # do not try to autoload if we already tried and it wasn't conclusive
        # as this is a time consuming operation. Warn the user.
        # Check first if debugging is on since the call to debug_once is expensive
        if Puppet[:debug]
          debug_once _("Not attempting to load %{type} %{fqname} as this object was missing during a prior compilation") % { type: type, fqname: fqname }
        end
      else
        fqname = munge_name(fqname)
        result = loader.try_load_fqname(type, fqname)
        @notfound[ fqname ] = result.nil?
      end
    end
    result
  end

  def munge_name(name)
    name.to_s.downcase
  end

  def dupe_check(instance, hash)
    return unless dupe = hash[instance.name]
    message = yield dupe
    instance.fail Puppet::ParseError, message
  end

  def dupe_check_singleton(instance, set)
    return if set.empty?
    message = yield set[0]
    instance.fail Puppet::ParseError, message
  end
end
