module HackTree
  # Context to execute the actions.
  class ActionContext
    # NOTE: No useless methods here, please.

    def initialize(instance, parent = nil)
      @instance, @parent = instance, parent

      # Suppress warnings.
      vrb, $VERBOSE = $VERBOSE, nil

      # Insert lookup routine for existing methods.
      (methods.map(&:to_s) - Node::FORBIDDEN_NAMES.map(&:to_s)).each do |method_name|
        next if not method_name =~ /\A(#{Node::NAME_REGEXP})\?{,1}\z/
        node_name = $1.to_sym
        instance_eval <<-EOT
          def #{method_name}(*args)
            if @instance.find_local_node(:#{node_name}, @parent)
              _dispatch(:#{method_name}, *args)
            else
              super
            end
          end
        EOT
      end

      # Restore warnings.
      $VERBOSE = vrb

      # Create direct methods. In case your console's completion is sane by itself, it will help.
      # IRB's default completion isn't sane yet (2012-02-25).
      @instance.nodes.select do |node|
        node.parent == @parent
      end.each do |node|
        # NOTE: This is slightly different from "existing method lookup routine" used above. Let's keep them separate.
        instance_eval <<-EOT
          def #{node.name}(*args)
            _dispatch(:#{node.name}, *args)
          end
        EOT
      end
    end

    def inspect
      # NOTES:
      #
      # * Exceptions raised here result in `(Object doesn't support #inspect)`. No other details are available, be careful.
      # * We don't return value from here, we **print** it directly.

      nodes = @instance.nodes.select {|node| node.parent == @parent}

      # Empty group?
      if nodes.empty?
        ::Kernel.puts "No groups/hacks here"
        return nil
      end

      # Not empty group, list contents.

      nodes = nodes.sort_by do |node|
        [
          node.is_a?(Node::Group) ? 0 : 1,    # Groups first.
          node.name.to_s,
        ]
      end

      # Compute name alignment width.
      names = nodes.map {|node| Tools.format_node_name(node)}
      name_align = Tools.compute_name_align(names, @instance.conf.local_name_align)

      nodes.each do |node|
        brief_desc = node.brief_desc || @instance.conf.brief_desc_stub

        fmt = "%-#{name_align}s%s"

        ::Kernel.puts(fmt % [
          Tools.format_node_name(node),
          brief_desc ? " # #{brief_desc}" : "",
        ])
      end # nodes.each

      nil
    end

    def method_missing(method_name, *args)
      _dispatch(method_name.to_sym, *args)
    end

    private

    #   _dispatch(:hello)     # Group/hack request.
    #   _dispatch(:hello?)    # Help request.
    def _dispatch(request, *args)
      raise ArgumentError, "Invalid request #{request.inspect}" if not request.to_s =~ /\A(#{Node::NAME_REGEXP})(\?{,1})\z/
      node_name = $1.to_sym
      is_question = ($2 != "")

      node = @instance.find_local_node(node_name, @parent)

      # NOTE: Method return result.
      if node
        if is_question
          # Help request.
          out = [
            node.brief_desc || @instance.conf.brief_desc_stub,
            (["", node.full_desc] if node.full_desc),
          ].flatten(1).compact

          # `out` are lines of text, eventually.
          ::Kernel.puts out.empty?? "No description, please provide one" : out

          # For groups list contents after description.
          #if node.is_a? Node::Group
          #  ::Kernel.puts ["", self.class.new(@instance, node).inspect]
          #end
        else
          # Group/hack request.
          case node
          when Node::Group
            # Create and return a new nested access context.
            self.class.new(@instance, node)
          when Node::Hack
            # Invoke hack in the context of `HackTree` instance.
            @instance.instance_exec(*args, &node.block)
          else
            raise "Unknown node class #{node.class}, SE"
          end
        end # if is_question
      else
        ::Kernel.puts "Node not found: '#{node_name}'"
      end
    end
  end # ActionContext
end