# encoding: utf-8
require 'sexp'

class Sexp
  # prepare current node.
  #
  # @param [RailsBestPractices::Core::CheckingVisitor] visitor the visitor to prepare current node
  def prepare(visitor)
    visitor.prepare(self)
  end

  # prepare current node.
  #
  # @param [RailsBestPractices::Core::CheckingVisitor] visitor the visitor to review current node
  def review(visitor)
    visitor.review(self)
  end

  # return child nodes of a sexp node.
  #
  #     s(:call, nil, :puts,
  #       s(:arglist, s(:str, "hello "), s(:str, "world"))
  #     )
  #       => [s(:arglist, s(:str, "hello "), s(:str, "world"))]
  #
  # @return [Array] child nodes.
  def children
    find_all { | sexp | Sexp === sexp }
  end

  # recursively find all child nodes, and yeild each child node.
  def recursive_children
    children.each do |child|
      yield child
      child.recursive_children { |c| yield c }
    end
  end

  # grep all the recursive child nodes with conditions, and yield each match node.
  #
  # @param [Hash] options grep conditions
  #
  # options is the grep conditions, like
  #
  #     :node_type => :call,
  #     :subject => s(:const, Post),
  #     :message => [:find, :new],
  #     :arguments => s(:arglist)
  #
  # the condition key is one of :node_type, :subject, :message or :arguments,
  # the condition value can be Symbol, Array or Sexp.
  def grep_nodes(options)
    node_type = options[:node_type]
    subject = options[:subject]
    message = options[:message]
    arguments = options[:arguments]
    self.recursive_children do |child|
      if (!node_type || (node_type.is_a?(Array) ? node_type.include?(child.node_type) : node_type == child.node_type)) &&
         (!subject || (subject.is_a?(Array) ? subject.include?(child.subject) : subject == child.subject)) &&
         (!message || (message.is_a?(Array) ? message.include?(child.message) : message == child.message)) &&
         (!arguments || (arguments.is_?(Array) ? arguments.include?(child.arguments) : arguments == child.arguments))
        yield child
      end
    end
  end

  # grep all the recursive child nodes with conditions, and yield the first match node.
  #
  # @param [Hash] options grep conditions
  #
  # options is the grep conditions, like
  #
  #     :node_type => :call,
  #     :subject => s(:const, Post),
  #     :message => [:find, :new],
  #     :arguments => s(:arglist)
  #
  # the condition key is one of :node_type, :subject, :message or :arguments,
  # the condition value can be Symbol, Array or Sexp.
  def grep_node(options)
    grep_nodes(options) { |node| return node }
  end

  # grep all the recursive child nodes with conditions, and get the count of match nodes.
  #
  # @param [Hash] options grep conditions
  # @return [Integer] the count of metch nodes
  def grep_nodes_count(options)
    count = 0
    grep_nodes(options) { |node| count += 1 }
    count
  end

  # Get subject of attrasgan, call and iter node.
  #
  #     s(:attrasgn,
  #       s(:call, nil, :user, s(:arglist)),
  #       :name=,
  #       s(:arglist,
  #         s(:call,
  #           s(:call, nil, :params, s(:arglist)),
  #           :[],
  #           s(:arglist, s(:lit, :name))
  #         )
  #       )
  #     )
  #         => s(:call, nil, :user, s(:arglist))
  #
  #     s(:call,
  #       s(:call, nil, :user, s(:arglist)),
  #       :name,
  #       s(:arglist)
  #     )
  #         => s(:call, nil, :user, s(:arglist))
  #
  #     s(:iter,
  #       s(:call, s(:ivar, :@users), :each, s(:arglist)),
  #       s(:lasgn, :user),
  #       s(:call, nil, :p,
  #         s(:arglist, s(:lvar, :user))
  #       )
  #     )
  #         => s(:call, :s(:ivar, ;@users), :each, s(:arglist))
  #
  # @return [Sexp] subject of attrasgn, call or iter node
  def subject
    if [:attrasgn, :call, :iter].include? node_type
      self[1]
    end
  end

  # Get the class name of the class node.
  #
  #     s(:class, :User, nil, s(:scope))
  #         => :User
  #
  # @return [Symbol] class name of class node
  def class_name
    if :class == node_type
      self[1]
    end
  end

  # Get the base class of the class node.
  #
  #     s(:class, :User, s(:colon2, s(:const, :ActiveRecord), :Base), s(:scope))
  #         => s(:colon2, s(:const, :ActiveRecord), :Base)
  #
  # @return [Sexp] base class of class node
  def base_class
    if :class == node_type
      self[2]
    end
  end

  # Get the left value of the lasgn or iasgn node.
  #
  #     s(:lasgn,
  #       :user,
  #       s(:call,
  #         s(:call, nil, :params, s(:arglist)),
  #         :[],
  #         s(:arglist, s(:lit, :user))
  #       )
  #     )
  #         => :user
  #
  #     s(:iasgn,
  #       :@user,
  #       s(:call,
  #         s(:call, nil, :params, s(:arglist)),
  #         :[],
  #         s(:arglist, s(:lit, :user))
  #       )
  #     )
  #         => :@user
  #
  # @return [Symbol] left value of lasgn or iasgn node
  def left_value
    if [:lasgn, :iasgn].include? node_type
      self[1]
    end
  end

  # Get the right value of lasgn and iasgn node.
  #
  #     s(:lasgn,
  #       :user,
  #       s(:call, nil, :current_user, s(:arglist))
  #     )
  #         => s(:call, nil, :current_user, s(:arglist))
  #
  #     s(:iasgn,
  #       :@user,
  #       s(:call, nil, :current_user, s(:arglist))
  #     )
  #         => s(:call, nil, :current_user, s(:arglist))
  #
  # @return [Sexp] right value of lasgn or iasgn node
  def right_value
    if [:lasgn, :iasgn].include? node_type
      self[2]
    end
  end

  # Get the message of attrasgn and call node.
  #
  #     s(:attrasgn,
  #       s(:call, nil, :user, s(:arglist)),
  #       :name=,
  #       s(:arglist,
  #         s(:call,
  #           s(:call, nil, :params, s(:arglist)),
  #           :[],
  #           s(:arglist, s(:lit, :name))
  #         )
  #       )
  #     )
  #         => :name=
  #
  #     s(:call, nil, :has_many, s(:arglist, s(:lit, :projects)))
  #         => :has_many
  #
  # @return [Symbol] message of attrasgn or call node
  def message
    if [:attrasgn, :call].include? node_type
      self[2]
    end
  end

  # Get arguments of call node.
  #
  #     s(:attrasgn,
  #       s(:call, nil, :post, s(:arglist)),
  #       :user=,
  #       s(:arglist,
  #         s(:call, nil, :current_user, s(:arglist))
  #       )
  #     )
  #         => s(:arglist, s(:call, nil, :current_user, s(:arglist)))
  #
  #     s(:call,
  #       s(:call, nil, :username, s(:arglist)),
  #       :==,
  #       s(:arglist, s(:str, ""))
  #     )
  #         => s(:arglist, s(:str, ""))
  #
  # @return [Sexp] arguments of attrasgn or call node
  def arguments
    if [:attrasgn, :call].include? node_type
      self[3]
    end
  end

  # Get the conditional statement of if node.
  #
  #     s(:if,
  #       s(:call,
  #         s(:call, nil, :current_user, s(:arglist)),
  #         :present?,
  #         s(:arglist)
  #       ),
  #       s(:call, nil, :puts,
  #         s(:arglist,
  #           s(:call,
  #             s(:call, nil, :current_user, s(:arglist)),
  #             :login,
  #             s(:arglist)
  #           )
  #         )
  #       ),
  #       nil
  #     )
  #         => s(:call, s(:call, nil, :current_user, s(:arglist)), :present?, s(:arglist))
  #
  # @return [Sexp] conditional statement of if node
  def conditional_statement
    if :if == node_type
      self[1]
    end
  end

  # Get the body node when conditional statement is true.
  #
  #     s(:if,
  #       s(:call, s(:call, nil, :current_user, s(:arglist)), :login?, s(:arglist)),
  #       s(:call, s(:call, nil, :current_user, s(:arglist)), :login, s(:arglist)),
  #       s(:call, s(:call, nil, :current_user, s(:arglist)), :email, s(:arglist))
  #     )
  #         => s(:call, s(:call, nil, :current_user, s(:arglist)), :login, s(:arglist))
  #
  # @return [Sexp] the body node when conditional statement is true
  def true_node
    if :if == node_type
      self[2]
    end
  end

  # Get the body node when conditional statement is false.
  #
  #     s(:if,
  #       s(:call, s(:call, nil, :current_user, s(:arglist)), :login?, s(:arglist)),
  #       s(:call, s(:call, nil, :current_user, s(:arglist)), :login, s(:arglist)),
  #       s(:call, s(:call, nil, :current_user, s(:arglist)), :email, s(:arglist))
  #     )
  #         => s(:call, s(:call, nil, :current_user, s(:arglist)), :email, s(:arglist))
  #
  # @return [Sexp] the body node when conditional statement is false
  def false_node
    if :if == node_type
      self[3]
    end
  end

  # Get the method name of defn node.
  #
  #     s(:defn, :show, s(:args), s(:scope, s(:block, s(:nil))))
  #         => :show
  #
  # @return [Symbol] method name of defn node
  def method_name
    if :defn == node_type
      self[1]
    end
  end

  # Get body of iter, class and defn node.
  #
  #     s(:iter,
  #       s(:call, nil, :resources, s(:arglist, s(:lit, :posts))),
  #       nil,
  #       s(:call, nil, :resources, s(:arglist, s(:lit, :comments)))
  #     )
  #         => s(:call, nil, :resources, s(:arglist, s(:lit, :comments)))
  #
  #     s(:class, :User, nil,
  #       s(:scope,
  #         s(:block,
  #           s(:defn, :login, s(:args), s(:scope, s(:block, s(:nil)))),
  #           s(:defn, :email, s(:args), s(:scope, s(:block, s(:nil))))
  #         )
  #       )
  #     )
  #         => s(:block,
  #              s(:defn, :login, s(:args), s(:scope, s(:block, s(:nil)))),
  #              s(:defn, :email, s(:args), s(:scope, s(:block, s(:nil))))
  #            )
  #
  #     s(:defn, :fullname, s(:args),
  #       s(:scope,
  #         s(:block,
  #           s(:call,
  #             s(:call,
  #               s(:call, nil, :first_name, s(:arglist)),
  #               :+,
  #               s(:arglist,
  #                 s(:call, nil, :last, s(:arglist))
  #               )
  #             ),
  #             :+,
  #             s(:arglist,
  #               s(:call, nil, :name, s(:arglist))
  #             )
  #           )
  #         )
  #       )
  #     )
  #         => s(:block,
  #              s(:call,
  #                s(:call,
  #                  s(:call, nil, :first_name, s(:arglist)),
  #                  :+,
  #                  s(:arglist,
  #                    s(:call, nil, :last, s(:arglist))
  #                  )
  #                ),
  #                :+,
  #                s(:arglist,
  #                  s(:call, nil, :name, s(:arglist))
  #                )
  #              )
  #            )
  #
  # @return [Sexp] body of iter, class or defn node
  def body
    if :iter == node_type
      self[3]
    elsif :class == node_type
      self[3][1]
    elsif :defn == node_type
      self[3][1]
    end
  end

  # to_s for lvar, ivar, lit, const, array, hash, and colon2 node.
  #
  # @param [Hash] options
  #   :remove_at remove the @ symbol for ivar.
  # @return [String] to_s
  def to_s(options={})
    case node_type
    when :true, :false, :nil
      self[0].to_s
    when :ivar
      options[:remove_at] ? self[1].to_s[1..-1] : self[1].to_s
    when :lvar, :str, :lit, :const
      self[1].to_s
    when :array
      "[\"#{self.children.collect(&:to_s).join('", "')}\"]"
    when :hash
      key_value = false # false is key, true is value
      result = ['{']
      children.each do |child|
        if [:true, :false, :nil, :array, :hash].include? child.node_type
          result << "#{child}"
        else
          result << "\"#{child}\""
        end
        result << (key_value ? ", " : " => ")
        key_value = !key_value
      end
      result.join("").sub(/, $/, '') + '}'
    when :colon2
      "#{self[1]}::#{self[2]}"
    else
      ""
    end
  end
end