require 'puppet/parser'
require 'puppet/util/warnings'
require 'puppet/util/errors'
require 'puppet/parser/ast/leaf'

# Puppet::Resource::Type represents nodes, classes and defined types.
#
# @api public
class Puppet::Resource::Type
  Puppet::ResourceType = self
  include Puppet::Util::Warnings
  include Puppet::Util::Errors

  RESOURCE_KINDS = [:hostclass, :node, :definition, :capability_mapping, :application, :site]

  # Map the names used in our documentation to the names used internally
  RESOURCE_KINDS_TO_EXTERNAL_NAMES = {
      :hostclass => "class",
      :node => "node",
      :definition => "defined_type",
      :application => "application",
      :site => 'site'
  }
  RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert

  NAME = 'name'.freeze
  TITLE = 'title'.freeze
  MODULE_NAME = 'module_name'.freeze
  CALLER_MODULE_NAME = 'caller_module_name'.freeze
  PARAMETERS = 'parameters'.freeze
  KIND = 'kind'.freeze
  NODES = 'nodes'.freeze
  DOUBLE_COLON = '::'.freeze
  EMPTY_ARRAY = [].freeze

  attr_accessor :file, :line, :doc, :code, :parent, :resource_type_collection
  attr_reader :namespace, :arguments, :behaves_like, :module_name

  # The attributes 'produces' and 'consumes' are arrays of the blueprints
  # of capabilities this type can produce/consume. The entries in the array
  # are a fairly direct representation of what goes into produces/consumes
  # clauses. Each entry is a hash with attributes
  #   :capability  - the type name of the capres produced/consumed
  #   :mappings    - a hash of attribute_name => Expression
  # These two attributes are populated in
  # PopsBridge::instantiate_CapabilityMapping

  # Map from argument (aka parameter) names to Puppet Type
  # @return [Hash<Symbol, Puppet::Pops::Types::PAnyType] map from name to type
  #
  attr_reader :argument_types

  # This should probably be renamed to 'kind' eventually, in accordance with the changes
  #  made for serialization and API usability (#14137).  At the moment that seems like
  #  it would touch a whole lot of places in the code, though.  --cprice 2012-04-23
  attr_reader :type

  RESOURCE_KINDS.each do |t|
    define_method("#{t}?") { self.type == t }
  end

  # Are we a child of the passed class?  Do a recursive search up our
  # parentage tree to figure it out.
  def child_of?(klass)
    return false unless parent

    return(klass == parent_type ? true : parent_type.child_of?(klass))
  end

  # Evaluate the resources produced by the given resource. These resources are
  # evaluated in a separate but identical scope from the rest of the resource.
  def evaluate_produces(resource, scope)
    # Only defined types and classes can produce capabilities
    return unless definition? || hostclass?

    resource.export.map do |ex|
      # Assert that the ref really is a resource reference
      raise Puppet::Error, _("Invalid export in %{reference}: %{ex} is not a resource") % { reference: resource.ref, ex: ex } unless ex.is_a?(Puppet::Resource)
      raise Puppet::Error, _("Invalid export in %{reference}: %{ex} is not a capability resource") % { reference: resource.ref, ex: ex } if ex.resource_type.nil? || !ex.resource_type.is_capability?

      blueprint = produces.find { |pr| pr[:capability] == ex.type }
      if blueprint.nil?
        raise Puppet::ParseError, _("Resource type %{res_type} does not produce %{ex_type}") % { res_type: resource.type, ex_type: ex.type }
      end
      t = ex.type
      t = Puppet::Pops::Evaluator::Runtime3ResourceSupport.find_resource_type(scope, t) unless t == 'class' || t == 'node'
      produced_resource = Puppet::Parser::Resource.new(t, ex.title, :scope => scope, :source => self)

      produced_resource.resource_type.parameters.each do |name|
        next if name == :name

        if expr = blueprint[:mappings][name.to_s]
          produced_resource[name] = expr.safeevaluate(scope)
        else
          produced_resource[name] = scope[name.to_s]
        end
      end
      # Tag the produced resource so we can later distinguish it from
      # copies of the resource that wind up in the catalogs of nodes that
      # use this resource. We tag the resource with producer:<environment>,
      # meaning produced resources need to be unique within their
      # environment
      # @todo lutter 2014-11-13: we would really like to use a dedicated
      # metadata field to indicate the producer of a resource, but that
      # requires changes to PuppetDB and its API; so for now, we just use
      # tagging
      produced_resource.tag("producer:#{scope.catalog.environment}")
      scope.catalog.add_resource(produced_resource)
      produced_resource[:require] = resource.ref
      produced_resource
    end
  end

  # Now evaluate the code associated with this class or definition.
  def evaluate_code(resource)

    static_parent = evaluate_parent_type(resource)
    scope = static_parent || resource.scope

    scope = scope.newscope(:source => self, :resource => resource) unless resource.title == :main
    scope.compiler.add_class(name) unless definition?

    set_resource_parameters(resource, scope)

    resource.add_edge_to_stage

    evaluate_produces(resource, scope)

    if code
      if @match # Only bother setting up the ephemeral scope if there are match variables to add into it
        scope.with_guarded_scope do
          scope.ephemeral_from(@match, file, line)
          code.safeevaluate(scope)
        end
      else
        code.safeevaluate(scope)
      end
    end
  end

  def initialize(type, name, options = {})
    @type = type.to_s.downcase.to_sym
    raise ArgumentError, _("Invalid resource supertype '%{type}'") % { type: type } unless RESOURCE_KINDS.include?(@type)

    name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName)

    set_name_and_namespace(name)

    [:code, :doc, :line, :file, :parent].each do |param|
      next unless value = options[param]
      send(param.to_s + '=', value)
    end

    set_arguments(options[:arguments])
    set_argument_types(options[:argument_types])

    @match = nil

    @module_name = options[:module_name]
  end

  def produces
    @produces || EMPTY_ARRAY
  end

  def consumes
    @consumes || EMPTY_ARRAY
  end

  def add_produces(blueprint)
    @produces ||= []
    @produces << blueprint
  end

  def add_consumes(blueprint)
    @consumes ||= []
    @consumes << blueprint
  end

  # This is only used for node names, and really only when the node name
  # is a regexp.
  def match(string)
    return string.to_s.downcase == name unless name_is_regex?

    @match = @name.match(string)
  end

  # Add code from a new instance to our code.
  def merge(other)
    fail _("%{name} is not a class; cannot add code to it") % { name: name } unless type == :hostclass
    fail _("%{name} is not a class; cannot add code from it") % { name: other.name } unless other.type == :hostclass
    if name == "" && Puppet.settings[:freeze_main]
      # It is ok to merge definitions into main even if freeze is on (definitions are nodes, classes, defines, functions, and types)
      unless other.code.is_definitions_only?
        fail _("Cannot have code outside of a class/node/define because 'freeze_main' is enabled")
      end
    end
    if parent and other.parent and parent != other.parent
      fail _("Cannot merge classes with different parent classes (%{name} => %{parent} vs. %{other_name} => %{other_parent})") % { name: name, parent: parent, other_name: other.name, other_parent: other.parent }
    end

    # We know they're either equal or only one is set, so keep whichever parent is specified.
    self.parent ||= other.parent

    if other.doc
      self.doc ||= ""
      self.doc += other.doc
    end

    # This might just be an empty, stub class.
    return unless other.code

    unless self.code
      self.code = other.code
      return
    end

    self.code = Puppet::Parser::ParserFactory.code_merger.concatenate([self, other])
  end

  # Make an instance of the resource type, and place it in the catalog
  # if it isn't in the catalog already.  This is only possible for
  # classes and nodes.  No parameters are be supplied--if this is a
  # parameterized class, then all parameters take on their default
  # values.
  def ensure_in_catalog(scope, parameters=nil)
    resource_type =
    case type
    when :definition
      raise ArgumentError, _('Cannot create resources for defined resource types')
    when :hostclass
      :class
    when :node
      :node
    when :site
      :site
    end

    # Do nothing if the resource already exists; this makes sure we don't
    # get multiple copies of the class resource, which helps provide the
    # singleton nature of classes.
    # we should not do this for classes with parameters
    # if parameters are passed, we should still try to create the resource
    # even if it exists so that we can fail
    # this prevents us from being able to combine param classes with include
    if parameters.nil?
      resource = scope.catalog.resource(resource_type, name)
      return resource unless resource.nil?
    elsif parameters.is_a?(Hash)
      parameters = parameters.map {|k, v| Puppet::Parser::Resource::Param.new(:name => k, :value => v, :source => self)}
    end
    resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self, :parameters => parameters)
    instantiate_resource(scope, resource)
    scope.compiler.add_resource(scope, resource)
    resource
  end

  def instantiate_resource(scope, resource)
    # Make sure our parent class has been evaluated, if we have one.
    if parent && !scope.catalog.resource(resource.type, parent)
      parent_type(scope).ensure_in_catalog(scope)
    end

    if ['Class', 'Node'].include? resource.type
      scope.catalog.merge_tags_from(resource)
    end
  end

  def name
    if type == :node && name_is_regex?
      "__node_regexp__#{@name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')}"
    else
      @name
    end
  end

  def name_is_regex?
    @name.is_a?(Regexp)
  end

  # @deprecated Not used by Puppet
  # @api private
  def assign_parameter_values(parameters, resource)
    Puppet.deprecation_warning(_('The method Puppet::Resource::Type.assign_parameter_values is deprecated and will be removed in the next major release of Puppet.'))

    return unless parameters

    # It'd be nice to assign default parameter values here,
    # but we can't because they often rely on local variables
    # created during set_resource_parameters.
    parameters.each do |name, value|
      resource.set_parameter name, value
    end
  end

  def parent_type(scope = nil)
    return nil unless parent

    @parent_type ||= scope.environment.known_resource_types.send("find_#{type}", parent) ||
      fail(Puppet::ParseError, _("Could not find parent resource type '%{parent}' of type %{parent_type} in %{env}") % { parent: parent, parent_type: type, env: scope.environment })
  end

  # Validate and set any arguments passed by the resource as variables in the scope.
  #
  # This method is known to only be used on the server/compile side.
  #
  # @param resource [Puppet::Parser::Resource] the resource
  # @param scope [Puppet::Parser::Scope] the scope
  #
  # @api private
  def set_resource_parameters(resource, scope)
    # Inject parameters from using external lookup
    modname = resource[:module_name] || module_name
    scope[MODULE_NAME] = modname unless modname.nil?
    caller_name = resource[:caller_module_name] || scope.parent_module_name
    scope[CALLER_MODULE_NAME] = caller_name unless caller_name.nil?

    resource.add_parameters_from_consume
    inject_external_parameters(resource, scope)

    if @type == :hostclass
      scope[TITLE] = resource.title.to_s.downcase
      scope[NAME] =  resource.name.to_s.downcase
    else
      scope[TITLE] = resource.title
      scope[NAME] =  resource.name
    end
    scope.class_set(self.name,scope) if hostclass? || node?

    param_hash = scope.with_parameter_scope(resource.to_s, arguments.keys) do |param_scope|
      # Assign directly to the parameter scope to avoid scope parameter validation at this point. It
      # will happen anyway when the values are assigned to the scope after the parameter scoped has
      # been popped.
      resource.each { |k, v| param_scope[k.to_s] = v.value unless k == :name || k == :title }
      assign_defaults(resource, param_scope, scope)
      param_scope.to_hash
    end

    validate_resource_hash(resource, param_hash)

    # Assign parameter values to current scope
    param_hash.each { |param, value| exceptwrap { scope[param] = value }}
  end

  # Lookup and inject parameters from external scope
  # @param resource [Puppet::Parser::Resource] the resource
  # @param scope [Puppet::Parser::Scope] the scope
  def inject_external_parameters(resource, scope)
    # Only lookup parameters for host classes
    return unless type == :hostclass
    parameters = resource.parameters
    arguments.each do |param_name, default|
      sym_name = param_name.to_sym
      param = parameters[sym_name]
      next unless param.nil? || param.value.nil?
      catch(:no_such_key) do
        bound_value = Puppet::Pops::Lookup.search_and_merge("#{name}::#{param_name}", Puppet::Pops::Lookup::Invocation.new(scope), nil)
        # Assign bound value but don't let an undef trump a default expression
        resource[sym_name] = bound_value unless bound_value.nil? && !default.nil?
      end
    end
  end
  private :inject_external_parameters

  def assign_defaults(resource, param_scope, scope)
    return unless resource.is_a?(Puppet::Parser::Resource)
    parameters = resource.parameters
    arguments.each do |param_name, default|
      next if default.nil?
      name = param_name.to_sym
      param = parameters[name]
      next unless param.nil? || param.value.nil?
      value = exceptwrap { param_scope.evaluate3x(param_name, default, scope) }
      resource[name] = value
      param_scope[param_name] = value
    end
  end
  private :assign_defaults

  def validate_resource_hash(resource, resource_hash)
    Puppet::Pops::Types::TypeMismatchDescriber.validate_parameters(resource.to_s, parameter_struct, resource_hash, resource.is_unevaluated_consumer?)
  end
  private :validate_resource_hash

  # Validate that all parameters given to the resource are correct
  # @param resource [Puppet::Resource] the resource to validate
  def validate_resource(resource)
    # Since Sensitive values have special encoding (in a separate parameter) an unwrapped sensitive value must be
    # recreated as a Sensitive in order to perform correct type checking.
    sensitives = Set.new(resource.sensitive_parameters)
    validate_resource_hash(resource,
      Hash[resource.parameters.map do |name, value|
        value_to_validate = sensitives.include?(name) ? Puppet::Pops::Types::PSensitiveType::Sensitive.new(value.value) : value.value
        [name.to_s, value_to_validate]
      end
    ])
  end

  # Check whether a given argument is valid.
  def valid_parameter?(param)
    parameter_struct.hashed_elements.include?(param.to_s)
  end

  def set_arguments(arguments)
    @arguments = {}
    @parameter_struct = nil
    return if arguments.nil?

    arguments.each do |arg, default|
      arg = arg.to_s
      warn_if_metaparam(arg, default)
      @arguments[arg] = default
    end
  end

  # Sets the argument name to Puppet Type hash used for type checking.
  # Names must correspond to available arguments (they must be defined first).
  # Arguments not mentioned will not be type-checked.
  #
  def set_argument_types(name_to_type_hash)
    @argument_types = {}
    @parameter_struct = nil
    return unless name_to_type_hash
    name_to_type_hash.each do |name, t|
      # catch internal errors
      unless @arguments.include?(name)
        raise Puppet::DevError, _("Parameter '%{name}' is given a type, but is not a valid parameter.") % { name: name }
      end
      unless t.is_a? Puppet::Pops::Types::PAnyType
        raise Puppet::DevError, _("Parameter '%{name}' is given a type that is not a Puppet Type, got %{class_name}") % { name: name, class_name: t.class }
      end
      @argument_types[name] = t
    end
  end

  # Returns boolean true if an instance of this type is a capability. This
  # implementation always returns false. This "duck-typing" interface is
  # shared among other classes and makes it easier to detect capabilities
  # when they are intermixed with non capability instances.
  def is_capability?
    false
  end

  private

  def convert_from_ast(name)
    value = name.value
    if value.is_a?(Puppet::Parser::AST::Regex)
      value.value
    else
      value
    end
  end

  def evaluate_parent_type(resource)
    return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name)
    parent_resource.evaluate unless parent_resource.evaluated?
    parent_scope(resource.scope, klass)
  end

  # Split an fq name into a namespace and name
  def namesplit(fullname)
    ary = fullname.split(DOUBLE_COLON)
    n = ary.pop || ""
    ns = ary.join(DOUBLE_COLON)
    return ns, n
  end

  def parent_scope(scope, klass)
    scope.class_scope(klass) || raise(Puppet::DevError, _("Could not find scope for %{class_name}") % { class_name: klass.name })
  end

  def set_name_and_namespace(name)
    if name.is_a?(Regexp)
      @name = name
      @namespace = ""
    else
      @name = name.to_s.downcase

      # Note we're doing something somewhat weird here -- we're setting
      # the class's namespace to its fully qualified name.  This means
      # anything inside that class starts looking in that namespace first.
      @namespace, _ = @type == :hostclass ? [@name, ''] : namesplit(@name)
    end
  end

  def warn_if_metaparam(param, default)
    return unless Puppet::Type.metaparamclass(param)

    if default
      warnonce _("%{param} is a metaparam; this value will inherit to all contained resources in the %{name} definition") % { param: param, name: self.name }
    else
      raise Puppet::ParseError, _("%{param} is a metaparameter; please choose another parameter name in the %{name} definition") % { param: param, name: self.name }
    end
  end

  def parameter_struct
    @parameter_struct ||= create_params_struct
  end

  def create_params_struct
    arg_types = argument_types
    type_factory = Puppet::Pops::Types::TypeFactory
    members = { type_factory.optional(type_factory.string(NAME)) =>  type_factory.any }

    if application?
      resource_type = type_factory.type_type(type_factory.resource)
      members[type_factory.string(NODES)] = type_factory.hash_of(type_factory.variant(
          resource_type, type_factory.array_of(resource_type)), type_factory.type_type(type_factory.resource('node')))
    end

    Puppet::Type.eachmetaparam do |name|
      # TODO: Once meta parameters are typed, this should change to reflect that type
      members[name.to_s] = type_factory.any
    end

    arguments.each_pair do |name, default|
      key_type = type_factory.string(name.to_s)
      key_type = type_factory.optional(key_type) unless default.nil?

      arg_type = arg_types[name]
      arg_type = type_factory.any if arg_type.nil?
      members[key_type] = arg_type
    end
    type_factory.struct(members)
  end
  private :create_params_struct
end
