module YARD
  module Parser
    module Ruby
      # Builds and s-expression by creating {AstNode} objects with
      # the type provided by the first argument.
      # 
      # @example An implicit list of keywords
      #   ast = s(s(:kw, "if"), s(:kw, "else"))
      #   ast.type # => :list
      # @example A method call
      #   s(:command, s(:var_ref, "mymethod"))
      # 
      # @overload s(*nodes, opts = {})
      #   @param [Array<AstNode>] nodes a list of nodes. 
      #   @param [Hash] opts any extra options (docstring, file, source) to
      #     set on the object
      #   @return [AstNode] an implicit node where node.type == +:list+
      # @overload s(type, *children, opts = {})
      #   @param [Symbol] type the node type
      #   @param [Array<AstNode>] children any child nodes inside this one
      #   @param [Hash] opts any extra options to set on the object
      #   @return [AstNode] a node of type +type+.
      # @see AstNode#initialize
      def s(*args)
        type = Symbol === args.first ? args.shift : :list
        opts = Hash === args.last ? args.pop : {}
        AstNode.node_class_for(type).new(type, args, opts)
      end
      
      # An AST node is characterized by a type and a list of children. It
      # is most easily represented by the s-expression {#s} such as:
      #   # AST for "if true; 5 end":
      #   s(s(:if, s(:var_ref, s(:kw, "true")), s(s(:int, "5")), nil))
      # 
      # The node type is not considered part of the list, only its children.
      # So +ast[0]+ does not refer to the type, but rather the first child
      # (or object). Items that are not +AstNode+ objects can be part of the
      # list, like Strings or Symbols representing names. To return only 
      # the AstNode children of the node, use {#children}.
      class AstNode < Array
        attr_accessor :type, :parent, :docstring, :docstring_range, :source
        attr_writer :source_range, :line_range, :file, :full_source
        alias comments docstring
        alias comments_range docstring_range
        alias to_s source
        
        # List of all known keywords
        # @return [Hash] 
        KEYWORDS = { class: true, alias: true, lambda: true, do_block: true,
          def: true, defs: true, begin: true, rescue: true, rescue_mod: true,
          if: true, if_mod: true, else: true, elsif: true, case: true,
          when: true, next: true, break: true, retry: true, redo: true,
          return: true, throw: true, catch: true, until: true, until_mod: true,
          while: true, while_mod: true, yield: true, yield0: true, zsuper: true,
          unless: true, unless_mod: true, for: true, super: true, return0: true }
        
        # Finds the node subclass that should be instantiated for a specific
        # node type
        # 
        # @param [Symbol] type the node type to find a subclass for
        # @return [Class] a subclass of AstNode to instantiate the node with.
        def self.node_class_for(type)
          case type
          when :params
            ParameterNode
          when :call, :fcall, :command, :command_call
            MethodCallNode
          when :if, :elsif, :if_mod, :unless, :unless_mod
            ConditionalNode
          when /_ref\Z/
            ReferenceNode
          else
            AstNode
          end
        end

        # Creates a new AST node
        # 
        # @param [Symbol] type the type of node being created
        # @param [Array<AstNode>] arr the child nodes
        # @param [Hash] opts any extra line options
        # @option opts [Fixnum] :line (nil) the line the node starts on in source
        # @option opts [String] :char (nil) the character number the node starts on
        #   in source
        # @option opts [Fixnum] :listline (nil) a special key like :line but for
        #   list nodes
        # @option opts [Fixnum] :listchar (nil) a special key like :char but for
        #   list nodes
        # @option opts [Boolean] :token (nil) whether the node represents a token
        def initialize(type, arr, opts = {})
          super(arr)
          self.type = type
          self.line_range = opts[:line]
          self.source_range = opts[:char]
          @fallback_line = opts[:listline]
          @fallback_source = opts[:listchar]
          @token = true if opts[:token]
        end
        
        # @return [Boolean] whether the node is equal to another by checking 
        #   the list and type
        def ==(ast)
          super && type == ast.type
        end
        
        # @return [String] the first line of source the node represents
        def show
          "\t#{line}: #{first_line}"
        end
        
        # @return [Range] the character range in {#full_source} represented
        #   by the node
        def source_range
          reset_line_info unless @source_range
          @source_range
        end
        
        # @return [Range] the line range in {#full_source} represented
        #   by the node
        def line_range
          reset_line_info unless @line_range
          @line_range
        end
        
        # @return [Boolean] whether the node has a {#line_range} set
        def has_line?
          @line_range ? true : false
        end
        
        # @return [Fixnum] the starting line number of the node
        def line
          line_range && line_range.first
        end
        
        # @return [String] the first line of source represented by the node.
        def first_line
          full_source.split(/\r?\n/)[line - 1].strip
        end
        
        # Searches through the node and all descendents and returns the
        # first node with a type matching any of +node_types+, otherwise
        # returns the original node (self).
        # 
        # @example Returns the first method definition in a block of code
        #   ast = YARD.parse_string("if true; def x; end end").ast
        #   ast.jump(:def)
        #   # => s(:def, s(:ident, "x"), s(:params, nil, nil, nil, nil, 
        #   #      nil), s(s(:void_stmt, )))
        # @example Returns first 'def' or 'class' statement
        #   ast = YARD.parse_string("class X; def y; end end")
        #   ast.jump(:def, :class).first
        #   # => 
        # @example If the node types are not present in the AST
        #   ast = YARD.parse("def x; end")
        #   ast.jump(:def)
        # 
        # @param [Array<Symbol>] node_types a set of node types to match
        # @return [AstNode] the matching node, if one was found
        # @return [self] if no node was found
        def jump(*node_types)
          traverse {|child| return(child) if node_types.include?(child.type) }
          self
        end

        # @return [Array<AstNode>] the {AstNode} children inside the node
        def children
          @children ||= select {|e| AstNode === e }
        end

        # @return [Boolean] whether the node is a token
        def token?
          @token
        end
        
        # @return [Boolean] whether the node is a reference (variable, 
        #   constant name)
        def ref?
          false
        end
        
        # @return [Boolean] whether the node is a literal value
        def literal?
          @literal ||= type =~ /_literal$/ ? true : false
        end
        
        # @return [Boolean] whether the node is a keyword
        def kw?
          @kw ||= KEYWORDS.has_key?(type)
        end
        
        # @return [Boolean] whether the node is a method call
        def call?
          false
        end
        
        # @return [Boolean] whether the node is a if/elsif/else condition
        def condition?
          false
        end

        # @return [String] the filename the node was parsed from
        def file
          return parent.file if parent
          @file
        end

        # @return [String] the full source that the node was parsed from
        def full_source
          return parent.full_source if parent
          return @full_source if @full_source
          return IO.read(@file) if file && File.exist?(file)
        end

        # @return [String] the parse of {#full_source} that the node represents
        def source
          return parent.full_source[source_range] if parent
          full_source
        end

        # @return [nil] pretty prints the node
        def pretty_print(q)
          objs = [*self.dup, :__last__]
          objs.unshift(type) if type && type != :list

          options = {}
          if @docstring
            options[:docstring] = docstring
          end
          if @source_range || @line_range
            options[:line] = line_range
            options[:source] = source_range
          end
          objs.pop if options.size == 0

          q.group(3, 's(', ')') do
            q.seplist(objs, nil, :each) do |v| 
              if v == :__last__
                q.seplist(options, nil, :each) do |k, v2| 
                  q.group(3) do 
                    q.text k
                    q.group(3) do 
                      q.text ': '
                      q.pp v2 
                    end
                  end
                end
              else
                q.pp v 
              end
            end
          end
        end

        # @return [String] inspects the object
        def inspect 
          typeinfo = type && type != :list ? ':' + type.to_s + ', ' : ''
          's(' + typeinfo + map(&:inspect).join(", ") + ')'
        end

        # Traverses the object and yields each node (including descendents) in order.
        # 
        # @yield each descendent node in order
        # @yieldparam [AstNode] self, or a child/descendent node
        # @return [void] 
        def traverse
          nodes = [self]
          nodes.each.with_index do |node, index|
            yield node
            nodes.insert index+1, *node.children
          end
        end
        
        private

        # Resets line information
        # @return [void] 
        def reset_line_info
          if size == 0
            self.line_range = @fallback_line
            self.source_range = @fallback_source
          elsif children.size > 0
            f, l = children.first, children.last
            self.line_range = Range.new(f.line_range.first, l.line_range.last)
            self.source_range = Range.new(f.source_range.first, l.source_range.last)
          elsif @fallback_line || @fallback_source
            self.line_range = @fallback_line
            self.source_range = @fallback_source
          else
            self.line_range = 0...0
            self.source_range = 0...0
          end
        end
      end
      
      class ReferenceNode < AstNode
        def ref?; true end
        
        def path
          Array.new flatten
        end
        
        def namespace
          Array.new flatten[0...-1]
        end
        
        def source
          super.split(/\s+/).first
        end
      end
      
      class ParameterNode < AstNode
        def required_params; self[0] end
        def required_end_params; self[3] end
        def optional_params; self[1] end
        def splat_param; self[2] ? self[2][0] : nil end
        def block_param; self[4] ? self[4][0] : nil end
      end
      
      class MethodCallNode < AstNode
        def call?; true end
        def namespace; first if index_adjust > 0 end

        def method_name(name_only = false)
          name = self[index_adjust]
          name_only ? name.jump(:ident).first.to_sym : name
        end

        def parameters(include_block_param = true)
          params = self[1 + index_adjust]
          return nil unless params
          params = call_has_paren? ? params.first : params
          include_block_param ? params : params[0...-1]
        end
        
        def block_param; parameters.last end
        
        private
        
        def index_adjust
          [:call, :command_call].include?(type) ? 2 : 0
        end
        
        def call_has_paren? 
          [:fcall, :call].include?(type)
        end
      end
      
      class ConditionalNode < AstNode
        def condition?; true end
        def condition; first end
        def then_block; self[1] end
        
        def else_block
          if self[2] && !cmod?
            self[2].type == :elsif ? self[2] : self[2][0]
          end
        end
        
        private
        
        def cmod?; type =~ /_mod$/ end
      end
    end
  end
end