# frozen_string_literal: true

require "prism"

module ActionviewPrecompiler
  module PrismASTParser
    # Each call object is responsible for holding a list of arguments and should
    # respond to a single #arguments_node method that returns an array of
    # arguments.
    class RenderCall
      attr_reader :argument_nodes

      def initialize(argument_nodes)
        @argument_nodes = argument_nodes
      end
    end

    # This class represents a node in the tree that is returned by the parser
    # that corresponds to an argument to a render call, or a child of one of
    # those nodes.
    class RenderNode
      attr_reader :node

      def initialize(node)
        @node = node
      end

      def call?
        node.is_a?(Prism::CallNode)
      end

      def hash?
        node.is_a?(Prism::HashNode) || node.is_a?(Prism::KeywordHashNode)
      end

      def string?
        node.is_a?(Prism::StringNode)
      end

      def symbol?
        node.is_a?(Prism::SymbolNode)
      end

      def variable_reference?
        case node.type
        when :class_variable_read_node, :instance_variable_read_node,
             :global_variable_read_node, :local_variable_read_node
          true
        else
          false
        end
      end

      def vcall?
        node.is_a?(Prism::CallNode) && node.variable_call?
      end

      def call_method_name
        node.message
      end

      def variable_name
        case node.type
        when :class_variable_read_node, :instance_variable_read_node,
             :global_variable_read_node, :local_variable_read_node
          node.name.name
        when :call_node
          node.message
        end
      end

      # Converts the node into a hash where the keys and values are nodes. This
      # will raise an error if the hash doesn't match the format we expect or
      # if the hash contains any splats.
      def to_hash
        if hash?
          if node.elements.all? { |assoc| assoc.is_a?(Prism::AssocNode) && assoc.key.is_a?(Prism::SymbolNode) }
            node.elements.to_h { |assoc| [RenderNode.new(assoc.key), RenderNode.new(assoc.value)] }
          end
        else
          raise CompilationError, "Unexpected node type: #{node.inspect}"
        end
      end

      # Converts the node into a string value. Only handles plain string
      # content, and will raise an error if the node contains interpolation.
      def to_string
        if string?
          node.unescaped
        else
          raise CompilationError, "Unexpected node type: #{node.inspect}"
        end
      end

      # Converts the node into a symbol value. Only handles labels and plain
      # symbols, and will raise an error if the node contains interpolation.
      def to_symbol
        if symbol?
          node.unescaped.to_sym
        else
          raise CompilationError, "Unexpected node type: #{node.inspect}"
        end
      end
    end

    # This visitor is responsible for visiting the parsed tree and extracting
    # out the render calls. After visiting the tree, the #render_calls method
    # will return the hash expected by the #parse_render_nodes method.
    class RenderVisitor < Prism::Visitor
      MESSAGE = /\A(render|render_to_string|layout)\z/

      attr_reader :render_calls

      def initialize
        @render_calls = Hash.new { |hash, key| hash[key] = [] }
      end

      def visit_call_node(node)
        if node.name.match?(MESSAGE) && !node.receiver && node.arguments
          args =
            node.arguments.arguments.map do |arg|
              if arg.is_a?(Prism::ParenthesesNode) && arg.body && arg.body.body.length == 1
                RenderNode.new(arg.body.body.first)
              else
                RenderNode.new(arg)
              end
            end

          render_calls[node.name.to_sym] << RenderCall.new(args)
        end

        super
      end
    end

    # Main entrypoint into this AST parser variant. It's responsible for
    # returning a hash of render calls. The keys are the method names, and the
    # values are arrays of call objects.
    def self.parse_render_nodes(code, filename)
      visitor = RenderVisitor.new
      result = Prism.parse(code)

      if result.success?
        result.value.accept(visitor)
        visitor.render_calls
      else
        raise CompilationError, "Unable to parse the template in #{filename}"
      end
    end
  end
end
