module Puppet::Pops::Lookup

# The ExplainNode contains information of a specific node in a tree traversed during
# lookup. The tree can be traversed using the `parent` and `branches` attributes of
# each node.
#
# Each leaf node contains information about what happened when the leaf of the branch
# was traversed.
  class ExplainNode
    def branches
      @branches ||= []
    end

    def to_hash
      hash = {}
      hash[:branches] = @branches.map {|b| b.to_hash} unless @branches.nil? || @branches.empty?
      hash
    end

    def to_s
      io = StringIO.new
      dump_on(io, '', '')
      io.string
    end

    def dump_on(io, indent, first_indent)
    end
  end

  class ExplainTreeNode < ExplainNode
    attr_reader :parent, :event, :key, :value

    def initialize(parent)
      @parent = parent
      @event = nil
    end

    def found_in_overrides(key, value)
      @key = key
      @value = value
      @event = :found_in_overrides
    end

    def found_in_defaults(key, value)
      @key = key
      @value = value
      @event = :found_in_defaults
    end

    def found(key, value)
      @key = key
      @value = value
      @event = :found
    end

    def result(value)
      @value = value
      @event = :result
    end

    def not_found(key)
      @key = key
      @event = :not_found
    end

    def path_not_found
      @event = :path_not_found
    end

    def module_not_found
      @event = :module_not_found
    end

    def increase_indent(indent)
      indent + '  '
    end

    def to_hash
      hash = super
      hash[:key] = @key unless @key.nil?
      hash[:value] = @value if [:found, :found_in_defaults, :found_in_overrides, :result].include?(@event)
      hash[:event] = @event unless @event.nil?
      hash[:type] = type
      hash
    end

    def type
      :root
    end

    def dump_outcome(io, indent)
      io << indent << 'No such key: "' << @key << "\"\n" if @event == :not_found
      if [:found, :found_in_overrides, :found_in_defaults].include?(@event)
        io << indent << 'Found key: "' << @key << '" value: '
        dump_value(io, indent, @value)
        io << ' in overrides' if @event == :found_in_overrides
        io << ' in defaults' if @event == :found_in_defaults
        io << "\n"
      end
    end

    def dump_value(io, indent, value)
      case value
      when Hash
        io << '{'
        unless value.empty?
          inner_indent = increase_indent(indent)
          value.reduce("\n") do |sep, (k, v)|
            io << sep << inner_indent
            dump_value(io, inner_indent, k)
            io << ' => '
            dump_value(io, inner_indent, v)
            ",\n"
          end
          io << "\n" << indent
        end
        io << '}'
      when Array
        io << '['
        unless value.empty?
          inner_indent = increase_indent(indent)
          value.reduce("\n") do |sep, v|
            io << sep << inner_indent
            dump_value(io, inner_indent, v)
            ",\n"
          end
          io << "\n" << indent
        end
        io << ']'
      else
        io << value.inspect
      end
    end
  end

  class ExplainInterpolate < ExplainTreeNode
    def initialize(parent, expression)
      super(parent)
      @expression = expression
    end

    def dump_on(io, indent, first_indent)
      io << first_indent << 'Interpolation on "' << @expression << "\"\n"
      indent = increase_indent(indent)
      branches.each {|b| b.dump_on(io, indent, indent)}
    end

    def to_hash
      hash = super
      hash[:expression] = @expression
      hash
    end

    def type
      :interpolate
    end
  end

  class ExplainMerge < ExplainTreeNode
    def initialize(parent, merge)
      super(parent)
      @merge = merge
    end

    def dump_on(io, indent, first_indent)
      # It's pointless to report a merge where there's only one branch
      return branches[0].dump_on(io, indent, first_indent) if branches.size == 1

      io << first_indent << 'Merge strategy ' << @merge.class.key << "\n"
      indent = increase_indent(indent)
      options = options_wo_strategy
      unless options.nil?
        io << indent << 'Options: '
        dump_value(io, indent, options)
        io << "\n"
      end
      branches.each {|b| b.dump_on(io, indent, indent)}
      if @event == :result
        io << indent << 'Merged result: '
        dump_value(io, indent, @value)
        io << "\n"
      end
    end

    def to_hash
      return branches[0].to_hash if branches.size == 1
      hash = super
      hash[:merge] = @merge.class.key
      options = options_wo_strategy
      hash[:options] = options unless options.nil?
      hash
    end

    def type
      :merge
    end

    def options_wo_strategy
      options = @merge.options
      if !options.nil? && options.include?('strategy')
        options = options.dup
        options.delete('strategy')
      end
      options.empty? ? nil : options
    end
  end

  class ExplainModule < ExplainTreeNode
    def initialize(parent, mod)
      super(parent)
      @module = mod
    end

    def dump_on(io, indent, first_indent)
      io << indent << 'Module "' << @module << '"'
      if branches.size == 1
        branches[0].dump_on(io, indent, ' using ')
      else
        io << "\n"
        indent = increase_indent(indent)
        branches.each {|b| b.dump_on(io, indent, indent)}
      end
      io << indent << "Module not found\n" if @event == :module_not_found
    end

    def to_hash
      if branches.size == 1
        branches[0].to_hash.merge(:module => @module)
      else
        hash = super
        hash[:module] = @module
        hash
      end
    end

    def type
      :module
    end
  end

  class ExplainGlobal < ExplainTreeNode
    def initialize(parent, binding_terminus)
      super(parent)
      @binding_terminus = binding_terminus
    end

    def dump_on(io, indent, first_indent)
      io << first_indent << 'Data Binding "' << @binding_terminus << "\"\n"
      indent = increase_indent(indent)
      branches.each {|b| b.dump_on(io, indent, indent)}
      dump_outcome(io, indent)
    end

    def to_hash
      hash = super
      hash[:name] = @binding_terminus
      hash
    end

    def type
      :global
    end
  end

  class ExplainDataProvider < ExplainTreeNode
    def initialize(parent, provider)
      super(parent)
      @provider = provider
    end

    def dump_on(io, indent, first_indent)
      io << first_indent << 'Data Provider "' << @provider.name << "\"\n"
      indent = increase_indent(indent)
      io << indent << 'ConfigurationPath "' << @provider.config_path << "\"\n" if @provider.respond_to?(:config_path)
      branches.each {|b| b.dump_on(io, indent, indent)}
      dump_outcome(io, indent)
    end

    def to_hash
      hash = super
      hash[:name] = @provider.name
      hash[:configuration_path] = @provider.config_path.to_s if @provider.respond_to?(:config_path)
      hash
    end

    def type
      :data_provider
    end
  end

  class ExplainPath < ExplainTreeNode
    def initialize(parent, path)
      super(parent)
      @path = path
    end

    def dump_on(io, indent, first_indent)
      io << indent << 'Path "' << @path.path << "\"\n"
      indent = increase_indent(indent)
      io << indent << 'Original path: ' << @path.original_path << "\n"
      branches.each {|b| b.dump_on(io, indent, indent)}
      io << indent << "Path not found\n" if @event == :path_not_found
      dump_outcome(io, indent)
    end

    def to_hash
      hash = super
      hash[:original_path] = @path.original_path
      hash[:path] = @path.path.to_s
      hash
    end

    def type
      :path
    end
  end

  class ExplainScope < ExplainTreeNode
    def initialize(parent)
      super(parent)
    end

    def dump_on(io, indent, first_indent)
      io << indent << 'Global Scope' << "\"\n"
      indent = increase_indent(indent)
      dump_outcome(io, indent)
    end

    def type
      :scope
    end
  end

  class Explainer < ExplainNode
    def initialize
      @current = self
    end

    def push(qualifier_type, qualifier)
      node = case (qualifier_type)
        when :global
         ExplainGlobal.new(@current, qualifier)
        when :path
          ExplainPath.new(@current, qualifier)
        when :module
          ExplainModule.new(@current, qualifier)
        when :interpolate
          ExplainInterpolate.new(@current, qualifier)
        when :data_provider
          ExplainDataProvider.new(@current, qualifier)
        when :merge
          ExplainMerge.new(@current, qualifier)
        when :scope
          ExplainScope.new(@current)
        else
          raise ArgumentError, "Unknown Explain type #{qualifier_type}"
        end
      @current.branches << node
      @current = node
    end

    def pop
      @current = @current.parent unless @current.parent.nil?
    end

    def accept_found_in_overrides(key, value)
      @current.found_in_overrides(key, value)
    end

    def accept_found_in_defaults(key, value)
      @current.found_in_defaults(key, value)
    end

    def accept_found(key, value)
      @current.found(key, value)
    end

    def accept_not_found(key)
      @current.not_found(key)
    end

    def accept_path_not_found
      @current.path_not_found
    end

    def accept_module_not_found
      @current.module_not_found
    end

    def accept_result(result)
      @current.result(result)
    end

    def dump_on(io, indent, first_indent)
      branches.each { |b| b.dump_on(io, indent, first_indent) }
    end

    def to_hash
      branches.size == 1 ? branches[0].to_hash : super
    end
  end
end
