module ActionviewPrecompiler
  module Ruby26ASTParser
    class Node
      def self.wrap(node)
        if RubyVM::AbstractSyntaxTree::Node === node
          new(node)
        else
          node
        end
      end

      def initialize(node)
        @node = node
      end

      def children
        @children ||= @node.children.map do |child|
          self.class.wrap(child)
        end
      end

      def inspect
        "#<#{self.class} #{@node.inspect}>"
      end

      def argument_nodes
        if children[1].array?
          children[1].children[0...-1]
        elsif children[1].block_pass?
          children[1].children[0].children[0...-1]
        else
          raise "can't call argument_nodes on #{inspect}"
        end
      end

      def array?
        type == :ARRAY || type == :LIST
      end

      def fcall?
        type == :FCALL
      end

      def hash?
        type == :HASH
      end

      def block_pass?
        type == :BLOCK_PASS
      end

      def string?
        type == :STR && String === children[0]
      end

      def variable_reference?
        type == :IVAR || type == :GVAR || type == :CVAR
      end

      def variable_name
        children[0].to_s
      end

      def vcall?
        type == :VCALL
      end

      def call?
        type == :CALL
      end

      def call_method_name
        children[1].to_s
      end

      def symbol?
        (type == :SYM) || (type == :LIT && Symbol === children[0])
      end

      def to_hash
        list = children[0]
        if list.nil?
          {}
        else
          hash = list.children[0..-2].each_slice(2).to_h
          return nil if hash.key?(nil)
          hash
        end
      end

      def to_string
        children[0]
      end

      def to_symbol
        children[0]
      end

      def fcall_named?(name)
        fcall? &&
          children[0] == name &&
          children[1] &&
          (children[1].array? || (children[1].block_pass? && children[1].children[0].array?))
      end

      private

      def type
        @node.type
      end
    end

    extend self

    METHODS_TO_PARSE = %i(render render_to_string layout)

    def parse_render_nodes(code, filename)
      renders = extract_render_nodes(parse(code))

      renders.group_by(&:first).collect do |method, nodes|
        [ method, nodes.collect { |v| v[1] } ]
      end.to_h
    rescue SyntaxError
      raise CompilationError, "Unable to parse the template in #{filename}"
    end

    def parse(code)
      Node.wrap(RubyVM::AbstractSyntaxTree.parse(code))
    end

    def node?(node)
      Node === node
    end

    def fcall?(node, name)
      node.fcall_named?(name)
    end

    def extract_render_nodes(root)
      renders = []
      queue = [root]

      while node = queue.shift
        node.children.each do |child|
          queue << child if node?(child)
        end

        method_name = render_call?(node)
        renders << [method_name, node] if method_name
      end

      renders
    end

    def render_call?(node)
      METHODS_TO_PARSE.detect { |m| fcall?(node, m) }
    end
  end
end
