require 'puppet-strings/yard/handlers/helpers'
require 'puppet-strings/yard/handlers/ruby/base'
require 'puppet-strings/yard/code_objects'
require 'puppet-strings/yard/util'

# Implements the handler for Puppet resource types written in Ruby.
class PuppetStrings::Yard::Handlers::Ruby::RsapiHandler < PuppetStrings::Yard::Handlers::Ruby::Base
  # The default docstring when ensurable is used without given a docstring.
  DEFAULT_ENSURABLE_DOCSTRING = 'The basic property that the resource should be in.'.freeze

  namespace_only
  handles method_call(:register_type)

  process do
    # Only accept calls to Puppet::ResourceApi
    return unless statement.count > 1
    module_name = statement[0].source
    return unless [ 'Puppet::ResourceApi' ].include? module_name

    schema = extract_schema

    # puts "Schema: #{schema.inspect}"

    object = PuppetStrings::Yard::CodeObjects::Type.new(schema['name'])
    register object

    docstring = schema['docs']
    if docstring
      register_docstring(object, PuppetStrings::Yard::Util.scrub_string(docstring.to_s), nil)
    else
      log.warn "Missing a description for Puppet resource type '#{object.name}' at #{statement.file}:#{statement.line}."
    end

    # Populate the parameters/properties/features to the type
    populate_type_data(object, schema)

    # Mark the type as public if it doesn't already have an api tag
    object.add_tag YARD::Tags::Tag.new(:api, 'public') unless object.has_tag? :api

    # Warn if a summary longer than 140 characters was provided
    PuppetStrings::Yard::Handlers::Helpers.validate_summary_tag(object) if object.has_tag? :summary
  end

  private

  def raise_parse_error(msg, location = statement)
    raise YARD::Parser::UndocumentableError, "#{msg} at #{location.file}:#{location.line}."
  end

  # check that the params of the register_type call are key/value pairs.
  def kv_arg_list?(params)
    params.type == :list && params.children.count > 0 && params.children.first.type == :list && params.children.first.children.count > 0 && statement.parameters.children.first.children.first.type == :assoc
  end

  def extract_schema
    raise_parse_error("Expected list of key/value pairs as argument") unless kv_arg_list?(statement.parameters)
    hash_from_node(statement.parameters.children.first)
  end

  def value_from_node(node)
    return nil unless node

    # puts "value from #{node.inspect}"

    case node.type
    when :int
      node.source.to_i
    when :hash
      hash_from_node(node)
    when :array
      array_from_node(node)
    when :var_ref
      var_ref_from_node(node)
    when :symbol, :symbol_literal, :label, :dyna_symbol, :string_literal
      node_as_string(node)
    else
      raise_parse_error("unexpected construct #{node.type}")
    end
  end

  def array_from_node(node)
    return nil unless node

    arr = node.children.collect do |assoc|
      value_from_node(assoc.children[0])
    end
  end

  def hash_from_node(node)
    return nil unless node

    # puts "hash from #{node.inspect}"

    kv_pairs = node.children.collect do |assoc|
      [ value_from_node(assoc.children[0]), value_from_node(assoc.children[1]) ]
    end
    Hash[kv_pairs]
  end

  def var_ref_from_node(node)
    return nil unless node

    # puts "var_ref from #{node.inspect}"

    if node.children.first.type == :kw
      case node.children.first.source
      when "false"
        return false
      when "true"
        return true
      when "nil"
        return nil
      else
        raise_parse_error("unexpected keyword '#{node.children.first.source}'")
      end
    end
    raise_parse_error("unexpected variable")
  end


  def populate_type_data(object, schema)
    return if schema['attributes'].nil?

    schema['attributes'].each do |name, definition|
      # puts "Processing #{name}: #{definition.inspect}"
      if ['parameter', 'namevar'].include? definition['behaviour']
        object.add_parameter(create_parameter(name, definition))
      else
        object.add_property(create_property(name, definition))
      end
    end
  end

  def create_parameter(name, definition)
    parameter = PuppetStrings::Yard::CodeObjects::Type::Parameter.new(name, definition['desc'])
    set_values(definition, parameter)
    parameter
  end

  def create_property(name, definition)
    property = PuppetStrings::Yard::CodeObjects::Type::Property.new(name, definition['desc'])
    set_values(definition, property)
    property
  end

  def set_values(definition, object)
    object.data_type = definition['type'] if definition.key? 'type'
    object.default = definition['default'] if definition.key? 'default'
    object.isnamevar = definition.key?('behaviour') && definition['behaviour'] == 'namevar'
  end
end
