require 'puppet/parser'
require 'puppet/util/warnings'
require 'puppet/util/errors'
require 'puppet/util/inline_docs'
require 'puppet/parser/ast/leaf'
require 'puppet/parser/ast/block_expression'
require 'puppet/dsl'

class Puppet::Resource::Type
  Puppet::ResourceType = self
  include Puppet::Util::InlineDocs
  include Puppet::Util::Warnings
  include Puppet::Util::Errors

  RESOURCE_KINDS = [:hostclass, :node, :definition]

  # We have reached a point where we've established some naming conventions
  #  in our documentation that don't entirely match up with our internal names
  #  for things.  Ideally we'd change the internal representation to match the
  #  conventions expressed in our docs, but that would be a fairly far-reaching
  #  and risky change.  For the time being, we're settling for mapping the
  #  internal names to the external ones (and vice-versa) during serialization
  #  and deserialization.  These two hashes is here to help with that mapping.
  RESOURCE_KINDS_TO_EXTERNAL_NAMES = {
      :hostclass => "class",
      :node => "node",
      :definition => "defined_type",
  }
  RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert

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

  # 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

  require 'puppet/indirector'
  extend Puppet::Indirector
  indirects :resource_type, :terminus_class => :parser

  def self.from_pson(data)
    name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified"
    kind = data.delete('kind') || "definition"

    unless type = RESOURCE_EXTERNAL_NAMES_TO_KINDS[kind]
      raise ArgumentError, "Unsupported resource kind '#{kind}'"
    end

    data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result }

    # This is a bit of a hack; when we serialize, we use the term "parameters" because that
    #  is the terminology that we use in our documentation.  However, internally to this
    #  class we use the term "arguments".  Ideally we'd change the implementation to be consistent
    #  with the documentation, but that would be challenging right now because it could potentially
    #  touch a lot of places in the code, not to mention that we already have another meaning for
    #  "parameters" internally.  So, for now, we will simply transform the internal "arguments"
    #  value to "parameters" when serializing, and the opposite when deserializing.
    #     --cprice 2012-04-23
    data[:arguments] = data.delete(:parameters)

    new(type, name, data)
  end

  # This method doesn't seem like it has anything to do with PSON in particular, and it shouldn't.
  #  It's just transforming to a simple object that can be serialized and de-serialized via
  #  any transport format.  Should probably be renamed if we get a chance to clean up our
  #  serialization / deserialization, and there are probably many other similar methods in
  #  other classes.
  #  --cprice 2012-04-23

  def to_pson_data_hash
    data = [:doc, :line, :file, :parent].inject({}) do |hash, param|
      next hash unless (value = self.send(param)) and (value != "")
      hash[param.to_s] = value
      hash
    end

    # This is a bit of a hack; when we serialize, we use the term "parameters" because that
    #  is the terminology that we use in our documentation.  However, internally to this
    #  class we use the term "arguments".  Ideally we'd change the implementation to be consistent
    #  with the documentation, but that would be challenging right now because it could potentially
    #  touch a lot of places in the code, not to mention that we already have another meaning for
    #  "parameters" internally.  So, for now, we will simply transform the internal "arguments"
    #  value to "parameters" when serializing, and the opposite when deserializing.
    #     --cprice 2012-04-23
    data['parameters'] = arguments.dup unless arguments.empty?

    data['name'] = name

    unless RESOURCE_KINDS_TO_EXTERNAL_NAMES.has_key?(type)
      raise ArgumentError, "Unsupported resource kind '#{type}'"
    end
    data['kind'] = RESOURCE_KINDS_TO_EXTERNAL_NAMES[type]
    data
  end

  # It seems wrong that we have a 'to_pson' method on this class, but not a 'to_yaml'.
  #  As a result, if you use the REST API to retrieve one or more objects of this type,
  #  you will receive different data if you use 'Accept: yaml' vs 'Accept: pson'.  That
  #  seems really, really wrong.  The "Accept" header should never affect what data is
  #  being returned--only the format of the data.  If the data itself is going to differ,
  #  then there should be a different request URL.  Documenting the REST API becomes
  #  a much more complex problem when the "Accept" header can change the semantics
  #  of the response.  --cprice 2012-04-23

  def to_pson(*args)
    to_pson_data_hash.to_pson(*args)
  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

  # 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(:namespace => namespace, :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

    code.safeevaluate(scope) if code

    evaluate_ruby_code(resource, scope) if ruby_code
  end

  def initialize(type, name, options = {})
    @type = type.to_s.downcase.to_sym
    raise ArgumentError, "Invalid resource supertype '#{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])

    @module_name = options[:module_name]
  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?

    @name =~ 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" unless type == :hostclass
    fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass
    fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main]

    if parent and other.parent and parent != other.parent
      fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{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::AST::BlockExpression.new(:children => [self.code, other.code])
#    array_class = Puppet::Parser::AST::ASTArray
#    self.code = array_class.new(:children => [self.code]) unless self.code.is_a?(array_class)
#
#    if other.code.is_a?(array_class)
#      code.children += other.code.children
#    else
#      code.children << other.code
#    end
  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)
    type == :definition and raise ArgumentError, "Cannot create resources for defined resource types"
    resource_type = type == :hostclass ? :class : :node

    # 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 resource = scope.catalog.resource(resource_type, name) and !parameters
      return resource
    end
    resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self)
    assign_parameter_values(parameters, resource)
    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.tag(*resource.tags)
    end
  end

  def name
    return @name unless @name.is_a?(Regexp)
    @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')
  end

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

  def assign_parameter_values(parameters, resource)
    return unless parameters
    scope = resource.scope || {}

    # 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

  # MQR TODO:
  #
  # The change(s) introduced by the fix for #4270 are mostly silly & should be
  # removed, though we didn't realize it at the time.  If it can be established/
  # ensured that nodes never call parent_type and that resource_types are always
  # (as they should be) members of exactly one resource_type_collection the
  # following method could / should be replaced with:
  #
  # def parent_type
  #   @parent_type ||= parent && (
  #     resource_type_collection.find_or_load([name],parent,type.to_sym) ||
  #     fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}"
  #   )
  # end
  #
  # ...and then the rest of the changes around passing in scope reverted.
  #
  def parent_type(scope = nil)
    return nil unless parent

    unless @parent_type
      raise "Must pass scope to parent_type when called first time" unless scope
      unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent)
        fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}"
      end
    end

    @parent_type
  end

  # Set any arguments passed by the resource as variables in the scope.
  def set_resource_parameters(resource, scope)
    set = {}
    resource.to_hash.each do |param, value|
      param = param.to_sym
      fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param)

      exceptwrap { scope[param.to_s] = value }

      set[param] = true
    end

    if @type == :hostclass
      scope["title"] = resource.title.to_s.downcase unless set.include? :title
      scope["name"] =  resource.name.to_s.downcase  unless set.include? :name
    else
      scope["title"] = resource.title               unless set.include? :title
      scope["name"] =  resource.name                unless set.include? :name
    end
    scope["module_name"] = module_name if module_name and ! set.include? :module_name

    if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name)
      scope["caller_module_name"] = caller_name
    end
    scope.class_set(self.name,scope) if hostclass? or node?

    # Evaluate the default parameters, now that all other variables are set
    default_params = resource.set_default_parameters(scope)
    default_params.each { |param| scope[param] = resource[param] }

    # This has to come after the above parameters so that default values
    # can use their values
    resource.validate_complete
  end

  # Check whether a given argument is valid.
  def valid_parameter?(param)
    param = param.to_s

    return true if param == "name"
    return true if Puppet::Type.metaparam?(param)
    return false unless defined?(@arguments)
    return(arguments.include?(param) ? true : false)
  end

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

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

  private

  def convert_from_ast(name)
    value = name.value
    if value.is_a?(Puppet::Parser::AST::Regex)
      name = value.value
    else
      name = 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

  def evaluate_ruby_code(resource, scope)
    Puppet::DSL::ResourceAPI.new(resource, scope, ruby_code).evaluate
  end

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

  def parent_scope(scope, klass)
    scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{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, ignored_shortname = @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 #{self.name} definition"
    else
      raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition"
    end
  end
end

