#!/usr/bin/env ruby -w

require "rubygems"
require "sexp_processor"

# :stopdoc:
# REFACTOR: stolen from ruby_parser
class Regexp
  unless defined? ENC_NONE then
    ENC_NONE = /x/n.options
    ENC_EUC  = /x/e.options
    ENC_SJIS = /x/s.options
    ENC_UTF8 = /x/u.options
  end

  unless defined? CODES then
    CODES = {
      EXTENDED   => "x",
      IGNORECASE => "i",
      MULTILINE  => "m",
      ENC_NONE   => "n",
      ENC_EUC    => "e",
      ENC_SJIS   => "s",
      ENC_UTF8   => "u",
    }
  end
end
# :startdoc:

##
# Generate ruby code from a sexp.

class Ruby2Ruby < SexpProcessor
  VERSION = "2.5.2" # :nodoc:

  # cutoff for one-liners
  LINE_LENGTH = 78

  # binary operation messages
  BINARY = [:<=>, :==, :<, :>, :<=, :>=, :-, :+, :*, :/, :%, :<<, :>>, :**, :"!=", :^, :|, :&]

  ##
  # Nodes that represent assignment and probably need () around them.
  #
  # TODO: this should be replaced with full precedence support :/

  ASSIGN_NODES = [
                  :dasgn,
                  :flip2,
                  :flip3,
                  :lasgn,
                  :masgn,
                  :match3,
                  :attrasgn,
                  :op_asgn1,
                  :op_asgn2,
                  :op_asgn_and,
                  :op_asgn_or,
                  :return,
                  :if, # HACK
                  :rescue,
                 ]

  ##
  # Some sexp types are OK without parens when appearing as hash values.
  # This list can include `:call`s because they're always printed with parens
  # around their arguments. For example:
  #
  #     { :foo => (bar("baz")) } # The outer parens are unnecessary
  #     { :foo => bar("baz") }   # This is the normal code style

  HASH_VAL_NO_PAREN = [
    :call,
    :false,
    :lit,
    :lvar,
    :nil,
    :str,
    :true,
  ]

  def initialize # :nodoc:
    super
    @indent = "  "
    self.require_empty = false
    self.strict = true
    self.expected = String

    @calls = []

    # self.debug[:defn] = /zsuper/
  end

  ############################################################
  # Processors

  def process_alias exp # :nodoc:
    _, lhs, rhs = exp

    parenthesize "alias #{process lhs} #{process rhs}"
  end

  def process_and exp # :nodoc:
    _, lhs, rhs = exp

    parenthesize "#{process lhs} and #{process rhs}"
  end

  def process_arglist exp # custom made node # :nodoc:
    _, *args = exp

    args.map { |arg|
      code = process arg
      arg.sexp_type == :rescue ? "(#{code})" : code
    }.join ", "
  end

  def process_args exp # :nodoc:
    _, *args = exp

    shadow = []

    args = args.map { |arg|
      case arg
      when Symbol then
        arg
      when Sexp then
        case arg.sexp_type
        when :lasgn then
          process(arg)
        when :masgn then
          process(arg)
        when :kwarg then
          _, k, v = arg
          "#{k}: #{process v}"
        when :shadow then
          shadow << arg.sexp_body
          next
        when :forward_args then
          "..."
        else
          raise "unknown arg type #{arg.first.inspect}"
        end
      when nil then
        ""
      else
        raise "unknown arg type #{arg.inspect}"
      end
    }.compact

    args   = args.join(", ").strip
    shadow = shadow.join(", ").strip
    shadow = "; #{shadow}" unless shadow.empty?

    "(%s%s)" % [args, shadow]
  end

  def process_array exp # :nodoc:
    "[#{process_arglist exp}]"
  end

  def process_attrasgn exp # :nodoc:
    _, recv, name, *args = exp

    rhs = args.pop
    args = s(:array, *args)
    receiver = process recv

    case name
    when :[]= then
      args = process args
      "#{receiver}#{args} = #{process rhs}"
    else
      raise "dunno what to do: #{args.inspect}" if args.size != 1 # s(:array)
      name = name.to_s.chomp "="
      if rhs && rhs != s(:arglist) then
        "#{receiver}.#{name} = #{process(rhs)}"
      else
        raise "dunno what to do: #{rhs.inspect}"
      end
    end
  end

  def process_back_ref exp # :nodoc:
    _, n = exp

    "$#{n}"
  end

  # TODO: figure out how to do rescue and ensure ENTIRELY w/o begin
  def process_begin exp # :nodoc:
    _, *rest = exp

    code = rest.map { |sexp|
      src = process sexp
      src = indent src unless src =~ /(^|\n)(rescue|ensure)/ # ensure no level 0 rescues
      src
    }
    code.unshift "begin"
    code.push "end"

    code.join "\n"
  end

  def process_block exp # :nodoc:
    _, *body = exp

    result = body.map { |sexp|
      process sexp
    }

    result << "# do nothing\n" if result.empty?
    result = parenthesize result.join "\n"
    result += "\n" unless result.start_with? "("

    result
  end

  def process_block_pass exp # :nodoc:
    raise "huh?: #{exp.inspect}" if exp.size > 2

    _, sexp = exp

    "&#{process sexp}"
  end

  def process_break exp # :nodoc:
    _, arg = exp

    val = process arg

    if val then
      "break #{val}"
    else
      "break"
    end
  end

  def process_call(exp, safe_call = false) # :nodoc:
    _, recv, name, *args = exp

    receiver_node_type = recv && recv.sexp_type
    receiver = process recv
    receiver = "(#{receiver})" if ASSIGN_NODES.include? receiver_node_type

    # args = []

    # this allows us to do both old and new sexp forms:
    # exp.push(*exp.pop[1..-1]) if exp.size == 1 && exp.first.first == :arglist

    @calls.push name

    in_context :arglist do
      max = args.size - 1
      args = args.map.with_index { |arg, i|
        arg_type = arg.sexp_type
        is_empty_hash = arg == s(:hash)
        arg = process arg

        next if arg.empty?

        strip_hash = (arg_type == :hash and
                      not BINARY.include? name and
                      not is_empty_hash and
                      (i == max or args[i + 1].sexp_type == :splat))
        wrap_arg = Ruby2Ruby::ASSIGN_NODES.include? arg_type

        arg = arg[2..-3] if strip_hash
        arg = "(#{arg})" if wrap_arg

        arg
      }.compact
    end

    case name
    when *BINARY then
      if safe_call
        "#{receiver}&.#{name}(#{args.join(", ")})"
      elsif args.length > 1
        "#{receiver}.#{name}(#{args.join(", ")})"
      else
        "(#{receiver} #{name} #{args.join(", ")})"
      end
    when :[] then
      receiver ||= "self"
      "#{receiver}[#{args.join(", ")}]"
    when :[]= then
      receiver ||= "self"
      rhs = args.pop
      "#{receiver}[#{args.join(", ")}] = #{rhs}"
    when :"!" then
      "(not #{receiver})"
    when :"-@" then
      "-#{receiver}"
    when :"+@" then
      "+#{receiver}"
    else
      args     = nil                    if args.empty?
      args     = "(#{args.join(", ")})" if args
      receiver = "#{receiver}."         if receiver and not safe_call
      receiver = "#{receiver}&."        if receiver and safe_call

      "#{receiver}#{name}#{args}"
    end
  ensure
    @calls.pop
  end

  def process_safe_call exp # :nodoc:
    process_call exp, :safe
  end

  def process_case exp # :nodoc:
    _, expr, *rest = exp

    result = []

    expr = process expr

    result << if expr then
                "case #{expr}"
              else
                "case"
              end

    result.concat rest.map { |pt|
      if pt and %i[when in].include? pt.sexp_type
        "#{process pt}"
      else
        next unless pt
        code = indent process pt
        code = indent("# do nothing") if code =~ /^\s*$/
        "else\n#{code}"
      end
    }

    result << "end"

    result.compact.join "\n"
  end

  def process_cdecl exp # :nodoc:
    _, lhs, rhs = exp
    lhs = process lhs if Sexp === lhs
    if rhs then
      rhs = process rhs
      "#{lhs} = #{rhs}"
    else
      lhs.to_s
    end
  end

  def process_class exp # :nodoc:
    "#{exp.comments}class #{util_module_or_class(exp, true)}"
  end

  def process_colon2 exp # :nodoc:
    _, lhs, rhs = exp

    "#{process lhs}::#{rhs}"
  end

  def process_colon3 exp # :nodoc:
    _, rhs = exp

    "::#{rhs}"
  end

  def process_const exp # :nodoc:
    _, name = exp

    name = process name if Sexp === name

    name.to_s
  end

  def __var name
    case context[1]
    when /_pat$/ then
      "^#{name}"
    when :in then
      "^#{name}"
    else
      name.to_s
    end
  end

  def process_cvar exp # :nodoc:
    _, name = exp

    __var name
  end

  def process_cvasgn exp # :nodoc:
    _, lhs, rhs = exp

    "#{lhs} = #{process rhs}"
  end

  def process_cvdecl exp # :nodoc:
    _, lhs, rhs = exp

    "#{lhs} = #{process rhs}"
  end

  def process_defined exp # :nodoc:
    _, rhs = exp
    "defined? #{process rhs}"
  end

  def process_defn(exp) # :nodoc:
    _, name, args, *body = exp

    comm = exp.comments
    args = process args
    args = "" if args == "()"

    body = s() if body == s(s(:nil)) # empty it out of a default nil expression

    # s(:defn, name, args, ivar|iasgn)
    case exp
    when s{ q(:defn, atom, t(:args), q(:ivar, atom)) } then # TODO: atom -> _
      _, ivar = body.first
      ivar = ivar.to_s[1..-1] # remove leading @
      reader = name.to_s
      return "attr_reader #{name.inspect}" if reader == ivar
    when s{ q(:defn, atom, t(:args), q(:iasgn, atom, q(:lvar, atom))) } then
      _, ivar, _val = body.first
      ivar = ivar.to_s[1..-1] # remove leading @
      reader = name.to_s.chomp "="
      return "attr_writer :#{reader}" if reader == ivar
    end

    body = body.map { |ssexp|
      process ssexp
    }

    simple = body.size <= 1

    body << "# do nothing" if body.empty?
    body = body.join("\n")
    body = body.lines.to_a[1..-2].join("\n") if
      simple && body =~ /^\Abegin/ && body =~ /^end\z/
    body = indent(body) unless simple && body =~ /(^|\n)rescue/

    "#{comm}def #{name}#{args}\n#{body}\nend".gsub(/\n\s*\n+/, "\n")
  end

  def process_defs exp # :nodoc:
    _, lhs, name, args, *body = exp
    var = [:self, :cvar, :dvar, :ivar, :gvar, :lvar].include? lhs.sexp_type

    lhs = process lhs
    lhs = "(#{lhs})" unless var

    name = "#{lhs}.#{name}"

    process_defn s(:defn, name, args, *body)
  end

  def process_dot2(exp) # :nodoc:
    _, lhs, rhs = exp

    "(#{process lhs}..#{process rhs})"
  end

  def process_dot3(exp) # :nodoc:
    _, lhs, rhs = exp

    "(#{process lhs}...#{process rhs})"
  end

  def process_dregx exp # :nodoc:
    _, str, *rest = exp

    options = re_opt rest.pop if Integer === rest.last

    "/" + util_dthing(:dregx, s(:dregx, str, *rest)) << "/#{options}"
  end

  def process_dregx_once(exp) # :nodoc:
    process_dregx(exp) + "o"
  end

  def process_dstr(exp) # :nodoc:
    "\"#{util_dthing(:dstr, exp)}\""
  end

  def process_dsym(exp) # :nodoc:
    ":\"#{util_dthing(:dsym, exp)}\""
  end

  def process_dxstr(exp) # :nodoc:
    "`#{util_dthing(:dxstr, exp)}`"
  end

  def process_ensure(exp) # :nodoc:
    _, body, ens = exp

    body = process body
    ens  = nil if ens == s(:nil)
    ens  = process(ens) || "# do nothing"
    ens = "begin\n#{ens}\nend\n" if ens =~ /(^|\n)rescue/

    body = body.sub(/\n\s*end\z/, "")
    body = indent(body) unless body =~ /(^|\n)rescue/

    "#{body}\nensure\n#{indent ens}"
  end

  def process_evstr exp # :nodoc:
    _, x = exp

    x ? process(x) : ""
  end

  def process_false exp # :nodoc:
    "false"
  end

  def process_flip2 exp # :nodoc:
    _, lhs, rhs = exp

    "#{process lhs}..#{process rhs}"
  end

  def process_flip3 exp # :nodoc:
    _, lhs, rhs = exp

    "#{process lhs}...#{process rhs}"
  end

  def process_for exp # :nodoc:
    _, recv, iter, body = exp

    recv = process recv
    iter = process iter
    body = process(body) || "# do nothing"

    result = ["for #{iter} in #{recv} do"]
    result << indent(body)
    result << "end"

    result.join "\n"
  end

  def process_forward_args exp
    "..."
  end

  def process_gasgn exp # :nodoc:
    process_iasgn exp
  end

  def process_gvar exp # :nodoc:
    _, name = exp

    __var name
  end

  def process_hash(exp) # :nodoc:
    _, *pairs = exp

    result = pairs.each_slice(2).map { |k, v|
      if k.sexp_type == :kwsplat then
        "%s" % process(k)
      elsif v.nil?
        # Shorthand hash syntax
        unless k.sexp_type == :lit and k.value.is_a? Symbol
          raise "Expected symbol for hash key, but got #{k.inspect}"
        end

        "#{k.value}:"
      else
        t = v.sexp_type

        lhs = process k
        rhs = process v
        rhs = "(#{rhs})" unless HASH_VAL_NO_PAREN.include? t

        "%s => %s" % [lhs, rhs]
      end
    }

    result.empty? ? "{}" : "{ #{result.join(", ")} }"
  end

  def process_iasgn(exp) # :nodoc:
    _, lhs, rhs = exp

    if rhs then
      "#{lhs} = #{process rhs}"
    else # part of an masgn
      lhs.to_s
    end
  end

  def process_in exp # :nodoc:
    _, lhs, *rhs = exp

    cond = process lhs
    body = rhs.compact.map { |sexp| indent process sexp }

    body << indent("# do nothing") if body.empty?
    body = body.join "\n"

    "in #{cond} then\n#{body.chomp}"
  end

  def process_find_pat exp # :nodoc:
    _, a, b, *c, d = exp

      a = process a               # might be nil
      b = process b if Sexp === b
      c = c.map { |sexp| process sexp }.join(", ")
      d = process d if Sexp === d

      "%s[%s, %s, %s]" % [a, b, c, d]
  end

  def process_array_pat exp # :nodoc:
    _, const, *rest = exp

    const = process const if const

    rest = rest.map { |sexp|
      case sexp
      when Sexp
        process sexp
      else
        sexp
      end
    }

    "%s[%s]" % [const, rest.join(", ")]
  end

  def process_kwrest exp # :nodoc:
    _, name = exp

    name.to_s
  end

  def process_hash_pat exp # :nodoc:
    _, const, *rest = exp

    if const then
      const = process const
      kwrest = process rest.pop if rest.last && rest.last.sexp_type == :kwrest

      rest = rest.each_slice(2).map { |k, v|
        k = process(k).delete_prefix ":"
        v = process v

        "#{k}: #{v}".strip
      }

      rest << kwrest if kwrest

      rest.empty? ? "%s[]" % const : "%s[%s]" % [const, rest.join(", ")]
    else
      kwrest = process rest.pop if rest.last && rest.last.sexp_type == :kwrest

      rest = rest.each_slice(2).map { |k, v|
        k = process(k).delete_prefix ":"
        v = process v

        "#{k}: #{v}".strip
      }

      rest << kwrest if kwrest

      rest.empty? ? "{}" : "{ %s }" % [rest.join(", ")]
    end
  end

  def process_preexe exp # :nodoc:
    "BEGIN"
  end

  def process_if(exp) # :nodoc:
    _, c, t, f = exp

    expand = Ruby2Ruby::ASSIGN_NODES.include? c.sexp_type
    c = process c
    t = process t
    f = process f

    c = "(#{c.chomp})" if c =~ /\n/

    if t then
      unless expand then
        if f then
          r = "#{c} ? (#{t}) : (#{f})"
          r = nil if r =~ /return/ # HACK - need contextual awareness or something
        else
          r = "#{t} if #{c}"
        end
        return r if r and (@indent + r).size < LINE_LENGTH and r !~ /\n/
      end

      r = []
      r << "if #{c} then\n#{indent(t)}\n"
      r << "else\n#{indent(f)}\n" if f
      r << "end"

      r.join
    elsif f
      unless expand then
        r = "#{f} unless #{c}"
        return r if (@indent + r).size < LINE_LENGTH and r !~ /\n/
      end
      "unless #{c} then\n#{indent(f)}\nend"
    else
      # empty if statement, just do it in case of side effects from condition
      "if #{c} then\n#{indent "# do nothing"}\nend"
    end
  end

  def process_lambda exp # :nodoc:
    "->"
  end

  MUST_BE_CURLY = %w[ BEGIN END ]

  def process_iter(exp) # :nodoc:
    _, iter, args, body = exp

    is_lambda = iter.sexp_type == :lambda

    iter = process iter
    body = process body if body

    args = case
           when args == 0 then
             ""
           when is_lambda then
             " (#{process(args)[1..-2]})"
           else
             " |#{process(args)[1..-2]}|"
           end

    b, e = if MUST_BE_CURLY.include? iter then
             %w[ { } ]
           else
             %w[ do end ]
           end

    iter = iter.sub(/\(\)$/, "")

    # REFACTOR: ugh
    result = []
    if is_lambda then
      result << iter
      result << args
      result << " {"
    else
      result << "#{iter} {"
      result << args
    end
    result << if body then
                " #{body.strip} "
              else
                " "
              end
    result << "}"
    result = result.join
    return result if result !~ /\n/ and result.size < LINE_LENGTH

    result = []

    if is_lambda then
      result << iter
      result << args
      result << " #{b}"
    else
      result << "#{iter} #{b}"
      result << args
    end

    result << "\n"
    if body then
      result << indent(body.strip)
      result << "\n"
    end
    result << e
    result.join
  end

  def process_ivar(exp) # :nodoc:
    _, name = exp

    __var name
  end

  def process_kwsplat(exp)
    _, kw = exp
    "**#{process kw}"
  end

  def process_lasgn(exp) # :nodoc:
    _, name, value = exp

    value = process value

    if value then
      "%s %s %s" % case context[1]
                   when /_pat$/ then
                     [value, "=>", name]
                   else
                     [name, "=", value]
                   end
    else
      "%s" % [name]
    end
  end

  def process_lit exp # :nodoc:
    _, obj = exp
    case obj
    when Range then
      "(#{obj.inspect})"
    else
      obj.inspect
    end
  end

  def process_lvar(exp) # :nodoc:
    _, name = exp

    __var name
  end

  def process_masgn(exp) # :nodoc:
    # s(:masgn, s(:array, s(:lasgn, :var), ...), s(:to_ary, <val>, ...))
    # s(:iter, <call>, s(:args, s(:masgn, :a, :b)), <body>)
    parenthesize = true

    result = exp.sexp_body.map { |sexp|
      case sexp
      when Sexp then
        if sexp.sexp_type == :array then
          parenthesize = context.grep(:masgn).size > 1
          res = process sexp

          res[1..-2]
        else
          process sexp
        end
      when Symbol then
        sexp
      else
        raise "unknown masgn: #{sexp.inspect}"
      end
    }
    parenthesize ? "(#{result.join ", "})" : result.join(" = ")
  end

  def process_match exp # :nodoc:
    _, rhs = exp

    "#{process rhs}"
  end

  def process_match2 exp # :nodoc:
    # s(:match2, s(:lit, /x/), s(:str, "blah"))
    _, lhs, rhs = exp

    lhs = process lhs
    rhs = process rhs

    "#{lhs} =~ #{rhs}"
  end

  def process_match3 exp # :nodoc:
    _, rhs, lhs = exp # yes, backwards

    left_type = lhs.sexp_type
    lhs = process lhs
    rhs = process rhs

    if ASSIGN_NODES.include? left_type then
      "(#{lhs}) =~ #{rhs}"
    else
      "#{lhs} =~ #{rhs}"
    end
  end

  def process_module(exp) # :nodoc:
    "#{exp.comments}module #{util_module_or_class(exp)}"
  end

  def process_next(exp) # :nodoc:
    _, rhs = exp

    val = rhs && process(rhs) # maybe push down into if and test rhs?
    if val then
      "next #{val}"
    else
      "next"
    end
  end

  def process_nil(exp) # :nodoc:
    "nil"
  end

  def process_not(exp) # :nodoc:
    _, sexp = exp
    "(not #{process sexp})"
  end

  def process_nth_ref(exp) # :nodoc:
    _, n = exp
    "$#{n}"
  end

  def process_op_asgn exp # :nodoc:
    case exp.length
    when 4
      _, lhs, op, rhs = exp

      lhs = process lhs
      rhs = process rhs

      "#{lhs} #{op}= #{rhs}"
    when 5
      _, lhs, rhs, index, op = exp

      lhs = process lhs
      rhs = process rhs

      "#{lhs}.#{index} #{op}= #{rhs}"
    else
      raise ArgumentError, "Don't know how to process this length: %p" % [exp]
    end
  end

  def process_op_asgn1(exp) # :nodoc:
    # [[:lvar, :b], [:arglist, [:lit, 1]], :"||", [:lit, 10]]
    _, lhs, index, msg, rhs = exp

    lhs   = process lhs
    index = process index
    rhs   = process rhs

    "#{lhs}[#{index}] #{msg}= #{rhs}"
  end

  def process_op_asgn2 exp # :nodoc:
    # [[:lvar, :c], :var=, :"||", [:lit, 20]]
    _, lhs, index, msg, rhs = exp

    lhs   = process lhs
    index = index.to_s[0..-2]
    rhs   = process rhs

    "#{lhs}.#{index} #{msg}= #{rhs}"
  end

  def process_op_asgn_and(exp) # :nodoc:
    # a &&= 1
    # [[:lvar, :a], [:lasgn, :a, [:lit, 1]]]
    _, _lhs, rhs = exp
    process(rhs).sub(/\=/, "&&=")
  end

  def process_op_asgn_or(exp) # :nodoc:
    # a ||= 1
    # [[:lvar, :a], [:lasgn, :a, [:lit, 1]]]
    _, _lhs, rhs = exp
    process(rhs).sub(/\=/, "||=")
  end

  def process_or(exp) # :nodoc:
    _, lhs, rhs = exp

    lhs = process lhs
    rhs = process rhs

    case context[1]
    when :in then
      "(#{lhs} | #{rhs})"
    else
      "(#{lhs} or #{rhs})"
    end
  end

  def process_postexe(exp) # :nodoc:
    "END"
  end

  def process_redo(exp) # :nodoc:
    "redo"
  end

  def process_resbody exp # :nodoc:
    # s(:resbody, s(:array), s(:return, s(:str, "a")))
    _, args, *body = exp

    body = body.compact.map { |sexp|
      process sexp
    }

    body << "# do nothing" if body.empty?

    name =   args.lasgn true
    name ||= args.iasgn true
    args = process(args)[1..-2]
    args = " #{args}" unless args.empty?
    args += " => #{name[1]}" if name

    "rescue#{args}\n#{indent body.join("\n")}"
  end

  def process_rescue exp # :nodoc:
    _, *rest = exp

    body = process rest.shift unless rest.first.sexp_type == :resbody
    els  = process rest.pop   unless rest.last && rest.last.sexp_type == :resbody

    body ||= "# do nothing"

    # TODO: I don't like this using method_missing, but I need to ensure tests
    simple = rest.size == 1 && rest.first.size <= 3 &&
      !rest.first.block &&
      !rest.first.return

    resbodies = rest.map { |resbody|
      _, rb_args, rb_body, *rb_rest = resbody
      simple &&= rb_args == s(:array)
      simple &&= rb_rest.empty? && rb_body && rb_body.node_type != :block
      process resbody
    }

    if els then
      "#{indent body}\n#{resbodies.join("\n")}\nelse\n#{indent els}"
    elsif simple then
      resbody = resbodies.first.sub(/\n\s*/, " ")
      "#{body} #{resbody}"
    else
      "#{indent body}\n#{resbodies.join("\n")}"
    end
  end

  def process_retry(exp) # :nodoc:
    "retry"
  end

  def process_return exp # :nodoc:
    _, rhs = exp

    unless rhs then
      "return"
    else
      rhs_type = rhs.sexp_type
      rhs = process rhs
      rhs = "(#{rhs})" if ASSIGN_NODES.include? rhs_type
      "return #{rhs}"
    end
  end

  def process_safe_attrasgn exp # :nodoc:
    _, receiver, name, *rest = exp

    receiver = process receiver
    rhs  = rest.pop
    args = rest.pop # should be nil

    raise "dunno what to do: #{args.inspect}" if args

    name = name.to_s.sub(/=$/, "")

    if rhs && rhs != s(:arglist) then
      "#{receiver}&.#{name} = #{process rhs}"
    else
      raise "dunno what to do: #{rhs.inspect}"
    end
  end

  def process_safe_op_asgn exp # :nodoc:
    # [[:lvar, :x], [:call, nil, :z, [:lit, 1]], :y, :"||"]
    _, lhs, rhs, index, op = exp

    lhs = process lhs
    rhs = process rhs

    "#{lhs}&.#{index} #{op}= #{rhs}"
  end

  def process_safe_op_asgn2(exp) # :nodoc:
    # [[:lvar, :c], :var=, :"||", [:lit, 20]]

    _, lhs, index, msg, rhs = exp

    lhs   = process lhs
    index = index.to_s[0..-2]
    rhs   = process rhs

    "#{lhs}&.#{index} #{msg}= #{rhs}"
  end

  def process_sclass(exp) # :nodoc:
    _, recv, *block = exp

    recv = process recv
    block = indent process_block s(:block, *block)

    "class << #{recv}\n#{block}\nend"
  end

  def process_self(exp) # :nodoc:
    "self"
  end

  def process_splat(exp) # :nodoc:
    _, arg = exp
    if arg.nil? then
      "*"
    else
      "*#{process arg}"
    end
  end

  def process_str(exp) # :nodoc:
    _, s = exp
    s.dump
  end

  def process_super(exp) # :nodoc:
    _, *args = exp

    args = args.map { |arg|
      process arg
    }

    "super(#{args.join ", "})"
  end

  def process_svalue(exp) # :nodoc:
    _, *args = exp

    args.map { |arg|
      process arg
    }.join ", "
  end

  def process_to_ary exp # :nodoc:
    _, sexp = exp

    process sexp
  end

  def process_true(exp) # :nodoc:
    "true"
  end

  def process_undef(exp) # :nodoc:
    _, name = exp

    "undef #{process name}"
  end

  def process_until(exp) # :nodoc:
    cond_loop(exp, "until")
  end

  def process_valias exp # :nodoc:
    _, lhs, rhs = exp

    "alias #{lhs} #{rhs}"
  end

  def process_when exp # :nodoc:
    _, lhs, *rhs = exp

    cond = process(lhs)[1..-2]

    rhs = rhs.compact.map { |sexp|
      indent process sexp
    }

    rhs << indent("# do nothing") if rhs.empty?
    rhs = rhs.join "\n"

    "when #{cond} then\n#{rhs.chomp}"
  end

  def process_while(exp) # :nodoc:
    cond_loop exp, "while"
  end

  def process_xstr(exp) # :nodoc:
    "`#{process_str(exp)[1..-2]}`"
  end

  def process_yield(exp) # :nodoc:
    _, *args = exp

    args = args.map { |arg|
      process arg
    }

    unless args.empty? then
      "yield(#{args.join(", ")})"
    else
      "yield"
    end
  end

  def process_zsuper(exp) # :nodoc:
    "super"
  end

  ############################################################
  # Rewriters:

  def rewrite_attrasgn exp # :nodoc:
    if context.first(2) == [:array, :masgn] then
      _, recv, msg, *args = exp

      exp = s(:call, recv, msg.to_s.chomp("=").to_sym, *args)
    end

    exp
  end

  def rewrite_call exp # :nodoc:
    _, recv, msg, *args = exp

    exp = s(:not, recv) if msg == :! && args.empty?

    exp
  end

  def rewrite_ensure exp # :nodoc:
    exp = s(:begin, exp) unless context.first == :begin
    exp
  end

  def rewrite_if exp # :nodoc:
    _, c, t, f = exp

    if c.sexp_type == :not then
      _, nc = c
      exp = s(:if, nc, f, t)
    end

    exp
  end

  def rewrite_resbody exp # :nodoc:
    _, args, *_rest = exp
    raise "no exception list in #{exp.inspect}" unless exp.size > 2 && args
    raise args.inspect if args.sexp_type != :array
    # for now, do nothing, just check and freak if we see an errant structure
    exp
  end

  def rewrite_rescue exp # :nodoc:
    complex = false
    complex ||= exp.size > 3
    complex ||= exp.resbody.block
    complex ||= exp.resbody.size > 3
    resbodies = exp.find_nodes(:resbody)
    complex ||= resbodies.any? { |n| n[1] != s(:array) }
    complex ||= resbodies.any? { |n| n.last.nil? }
    complex ||= resbodies.any? { |(_, _, body)| body and body.node_type == :block }

    handled = context.first == :ensure

    exp = s(:begin, exp) if complex unless handled

    exp
  end

  def rewrite_svalue(exp) # :nodoc:
    case exp.last.sexp_type
    when :array
      s(:svalue, *exp[1].sexp_body)
    when :splat
      exp
    else
      raise "huh: #{exp.inspect}"
    end
  end

  def rewrite_until exp # :nodoc:
    _, c, *body = exp

    if c.sexp_type == :not then
      _, nc = c
      exp = s(:while, nc, *body)
    end

    exp
  end

  def rewrite_while exp # :nodoc:
    _, c, *body = exp

    if c.sexp_type == :not then
      _, nc = c
      exp = s(:until, nc, *body)
    end

    exp
  end

  ############################################################
  # Utility Methods:

  ##
  # Generate a post-or-pre conditional loop.

  def cond_loop(exp, name)
    _, cond, body, head_controlled = exp

    cond = process cond
    body = process body

    body = indent(body).chomp if body

    code = []
    if head_controlled then
      code << "#{name} #{cond} do"
      code << body if body
      code << "end"
    else
      code << "begin"
      code << body if body
      code << "end #{name} #{cond}"
    end

    code.join("\n")
  end

  ##
  # Utility method to escape something interpolated.

  def dthing_escape type, lit
    # TODO: this needs more testing
    case type
    when :dregx then
      lit.gsub(/(\A|[^\\])\//, '\1\/')
    when :dstr, :dsym then
      lit.dump[1..-2]
    when :dxstr then
      lit.gsub(/`/, '\`')
    else
      raise "unsupported type #{type.inspect}"
    end
  end

  ##
  # Indent all lines of +s+ to the current indent level.

  def indent s
    s.to_s.split(/\n/).map{|line| @indent + line}.join("\n")
  end

  ##
  # Wrap appropriate expressions in matching parens.

  def parenthesize exp
    case context[1]
    when nil, :defn, :defs, :class, :sclass, :if, :iter, :resbody, :when, :while then
      exp
    else
      "(#{exp})"
    end
  end

  ##
  # Return the appropriate regexp flags for a given numeric code.

  def re_opt options
    bits = (0..8).map { |n| options[n] * 2**n }
    bits.delete 0
    bits.map { |n| Regexp::CODES[n] }.join
  end

  ##
  # Return a splatted symbol for +sym+.

  def splat(sym)
    :"*#{sym}"
  end

  ##
  # Utility method to generate something interpolated.

  def util_dthing(type, exp)
    _, str, *rest = exp

    # first item in sexp is a string literal
    str = dthing_escape(type, str)

    rest = rest.map { |pt|
      case pt.sexp_type
      when :str then
        dthing_escape(type, pt.last)
      when :evstr then
        '#{%s}' % [process(pt)]
      else
        raise "unknown type: #{pt.inspect}"
      end
    }

    [str, rest].join
  end

  ##
  # Utility method to generate ether a module or class.

  def util_module_or_class exp, is_class = false
    result = []

    _, name, *body = exp
    superk = body.shift if is_class

    name = process name if Sexp === name

    result << name

    if superk then
      superk = process superk
      result << " < #{superk}" if superk
    end

    result << "\n"

    body = body.map { |sexp|
      process(sexp).chomp
    }

    body = unless body.empty? then
             indent(body.join("\n\n")) + "\n"
           else
             ""
           end

    result << body
    result << "end"

    result.join
  end
end
