require 'ruby2js'

module Ruby2JS
  module Filter
    module Functions
      include SEXP

      VAR_TO_ASSIGN = {
        lvar: :lvasgn,
        ivar: :ivasgn,
        cvar: :cvasgn,
        gvar: :gvasgn
      }

      def on_send(node)
        target, method, *args = node.children

        if [:max, :min].include? method and args.length == 0
          return super unless node.is_method?
          process S(:send, s(:attr, s(:const, nil, :Math), node.children[1]),
            :apply, s(:const, nil, :Math), target)

        elsif method == :call and target and target.type == :ivar
          process S(:send, s(:self), "_#{target.children.first.to_s[1..-1]}",
            *args)

        elsif method == :call and target and target.type == :cvar
          process S(:send, s(:attr, s(:self), :constructor), 
            "_#{target.children.first.to_s[2..-1]}", *args)

        elsif method == :keys and args.length == 0 and node.is_method?
          process S(:send, s(:const, nil, :Object), :keys, target)

        elsif method == :merge!
          process S(:send, s(:const, nil, :Object), :assign, target, *args)

        elsif method == :delete and args.length == 1
          if not target
            process S(:undef, args.first)
          elsif args.first.type == :str
            process S(:undef, S(:attr, target, args.first.children.first))
          else
            process S(:undef, S(:send, target, :[], args.first))
          end

        elsif method == :to_s
          process S(:call, target, :toString, *args)

        elsif method == :Array and target == nil
          process S(:send, s(:attr, s(:attr, s(:const, nil, :Array), 
            :prototype), :slice), :call, *args)

        elsif method == :to_i
          process node.updated :send, [nil, :parseInt, target, *args]

        elsif method == :to_f
          process node.updated :send, [nil, :parseFloat, target, *args]

        elsif method == :sub and args.length == 2
          process node.updated nil, [target, :replace, *args]

        elsif [:sub!, :gsub!].include? method
          method = :"#{method.to_s[0..-2]}"
          if VAR_TO_ASSIGN.keys.include? target.type
            process S(VAR_TO_ASSIGN[target.type], target.children[0], 
              S(:send, target, method, *node.children[2..-1]))
          elsif target.type == :send
            if target.children[0] == nil
              process S(:lvasgn, target.children[1], S(:send,
                S(:lvar, target.children[1]), method, *node.children[2..-1]))
            else
              process S(:send, target.children[0], :"#{target.children[1]}=", 
                S(:send, target, method, *node.children[2..-1]))
            end
          else
            super
          end

        elsif method == :gsub and args.length == 2
          before, after = args
          if before.type == :regexp
            before = before.updated(:regexp, [*before.children[0...-1],
              s(:regopt, :g, *before.children.last)])
          elsif before.type == :str
            before = before.updated(:regexp,
              [s(:str, Regexp.escape(before.children.first)), s(:regopt, :g)])
          end
          process node.updated nil, [target, :replace, before, after]

        elsif method == :ord and args.length == 0
          if target.type == :str
            process S(:int, target.children.last.ord)
          else
            process S(:send, target, :charCodeAt, s(:int, 0))
          end

        elsif method == :chr and args.length == 0
          if target.type == :int
            process S(:str, target.children.last.chr)
          else
            process S(:send, s(:const, nil, :String), :fromCharCode, target)
          end

        elsif method == :empty? and args.length == 0
          process S(:send, S(:attr, target, :length), :==, s(:int, 0))

        elsif method == :nil? and args.length == 0
          process S(:send, target, :==, s(:nil))

        elsif [:start_with?, :end_with?].include? method and args.length == 1
          if args.first.type == :str
            length = S(:int, args.first.children.first.length)
          else
            length = S(:attr, *args, :length)
          end

          if method == :start_with?
            process S(:send, S(:send, target, :substring, s(:int, 0), 
              length), :==, *args)
          else
            process S(:send, S(:send, target, :slice, 
              S(:send, length, :-@)), :==, *args)
          end

        elsif method == :clear and args.length == 0 and node.is_method?
          process S(:send, target, :length=, s(:int, 0))

        elsif method == :replace and args.length == 1
          process S(:begin, S(:send, target, :length=, s(:int, 0)),
             S(:send, target, :push, s(:splat, node.children[2])))

        elsif method == :include? and args.length == 1
          process S(:send, S(:send, target, :indexOf, args.first), :!=,
            s(:int, -1))

        elsif method == :respond_to? and args.length == 1
          process S(:in?, args.first, target)

        elsif method == :each
          process S(:send, target, :forEach, *args)

        elsif method == :downcase and args.length == 0
          process S(:send, target, :toLowerCase)

        elsif method == :upcase and args.length == 0
          process S(:send, target, :toUpperCase)

        elsif method == :strip and args.length == 0
          process S(:send, target, :trim)

        elsif node.children[0..1] == [nil, :puts]
          process S(:send, s(:attr, nil, :console), :log, *args)

        elsif method == :first
          if node.children.length == 2
            process S(:send, target, :[], s(:int, 0))
          elsif node.children.length == 3
            process on_send S(:send, target, :[], s(:erange,
              s(:int, 0), node.children[2]))
          else
            super
          end

        elsif method == :last
          if node.children.length == 2
            process on_send S(:send, target, :[], s(:int, -1))
          elsif node.children.length == 3
            process S(:send, target, :slice,
              s(:send, s(:attr, target, :length), :-, node.children[2]),
              s(:attr, target, :length))
          else
            super
          end


        elsif method == :[]
          index = args.first

          # resolve negative literal indexes
          i = proc do |index|
            if index.type == :int and index.children.first < 0
              process S(:send, S(:attr, target, :length), :-, 
                s(:int, -index.children.first))
            else
              index
            end
          end

          if not index
            super

          elsif index.type == :regexp
            process S(:send, S(:send, target, :match, index), :[], 
              args[1] || s(:int, 0))

          elsif node.children.length != 3
            super

          elsif index.type == :int and index.children.first < 0
            process S(:send, target, :[], i.(index))

          elsif index.type == :erange
            start, finish = index.children
            process S(:send, target, :slice, i.(start), i.(finish))

          elsif index.type == :irange
            start, finish = index.children
            start = i.(start)
            if finish.type == :int
              if finish.children.first == -1
                finish = S(:attr, target, :length)
              else
                finish = i.(S(:int, finish.children.first+1))
              end
            else
              finish = S(:send, finish, :+, s(:int, 1))
            end
            process S(:send, target, :slice, start, finish)

          else
            super
          end

        elsif method == :reverse! and node.is_method? 
          # input: a.reverse!
          # output: a.splice(0, a.length, *a.reverse)
          process S(:send, target, :splice, s(:int, 0), 
            s(:attr, target, :length), s(:splat, S(:send, target, 
            :reverse, *node.children[2..-1])))

        elsif method == :each_with_index
          process S(:send, target, :forEach, *args)

        elsif method == :inspect and args.length == 0
          S(:send, s(:const, nil, :JSON), :stringify, process(target))

        elsif method == :* and target.type == :str
          process S(:send, s(:send, s(:const, nil, :Array), :new,
            s(:send, args.first, :+, s(:int, 1))), :join, target)

        elsif [:is_a?, :kind_of?].include? method and args.length == 1
          if args[0].type == :const
            parent = args[0].children.last
            parent = :Number if parent == :Float
            parent = :Object if parent == :Hash
            parent = :Function if parent == :Proc
            parent = :Error if parent == :Exception
            parent = :RegExp if parent == :Regexp
            if parent == :Array
              S(:send, s(:const, nil, :Array), :isArray, target)
            elsif [:Arguments, :Boolean, :Date, :Error, :Function, :Number,
                :Object, :RegExp, :String].include? parent
              S(:send, s(:send, s(:attr, s(:attr, s(:const, nil, Object), 
                :prototype), :toString), :call, target), :===,
                s(:str, "[object #{parent.to_s}]"))
            else
              super
            end
          else
            super
          end

        elsif target && target.type == :send and target.children[1] == :delete
          # prevent chained delete methods from being converted to undef
          S(:send, target.updated(:sendw), *node.children[1..-1])

        else
          super
        end
      end

      def on_block(node)
        call = node.children.first
        if [:setInterval, :setTimeout].include? call.children[1]
          return super unless call.children.first == nil
          block = process s(:block, s(:send, nil, :proc), *node.children[1..-1])
          on_send call.updated nil, [*call.children[0..1], block,
            *call.children[2..-1]]

        elsif [:sub, :gsub, :sub!, :gsub!].include? call.children[1]
          return super if call.children.first == nil
          block = s(:block, s(:send, nil, :proc), node.children[1],
            s(:autoreturn, *node.children[2..-1]))
          process call.updated(nil, [*call.children, block])

        elsif call.children[1] == :select and call.children.length == 2
          call = call.updated nil, [call.children.first, :filter]
          node.updated nil, [process(call), process(node.children[1]),
            s(:autoreturn, *process_all(node.children[2..-1]))]

        elsif call.children[1] == :any? and call.children.length == 2
          call = call.updated nil, [call.children.first, :some]
          node.updated nil, [process(call), process(node.children[1]),
            s(:autoreturn, *process_all(node.children[2..-1]))]

        elsif call.children[1] == :all? and call.children.length == 2
          call = call.updated nil, [call.children.first, :every]
          node.updated nil, [process(call), process(node.children[1]),
            s(:autoreturn, *process_all(node.children[2..-1]))]

        elsif call.children[1] == :find and call.children.length == 2
          node.updated nil, [process(call), process(node.children[1]),
            s(:autoreturn, *process_all(node.children[2..-1]))]

        elsif call.children[1] == :find_index and call.children.length == 2
          call = call.updated nil, [call.children.first, :findIndex]
          node.updated nil, [process(call), process(node.children[1]),
            s(:autoreturn, *process_all(node.children[2..-1]))]

        elsif call.children[1] == :map and call.children.length == 2
          node.updated nil, [process(call), process(node.children[1]),
            s(:autoreturn, *process_all(node.children[2..-1]))]

        elsif [:map!, :select!].include? call.children[1]
          # input: a.map! {expression}
          # output: a.splice(0, a.length, *a.map {expression})
          method = (call.children[1] == :map! ? :map : :select)
          target = call.children.first
          process call.updated(:send, [target, :splice, s(:splat, s(:send, 
            s(:array, s(:int, 0), s(:attr, target, :length)), :concat,
            s(:block, s(:send, target, method, *call.children[2..-1]),
            *node.children[1..-1])))])

        elsif node.children[0..1] == [s(:send, nil, :loop), s(:args)]
          # input: loop {statements}
          # output: while(true) {statements}
          S(:while, s(:true), node.children[2])

        elsif call.children[1] == :delete
          # restore delete methods that are prematurely mapped to undef
          result = super

          if result.children[0].type == :undef
            call = result.children[0].children[0]
            if call.type == :attr
              call = call.updated(:send, 
                [call.children[0], :delete, s(:str, call.children[1])])
              result = result.updated(nil, [call, *result.children[1..-1]])
            else
              call = call.updated(nil, 
                [call.children[0], :delete, *call.children[2..-1]])
              result = result.updated(nil, [call, *result.children[1..-1]])
            end
          end

          result

        elsif call.children[1] == :downto
          range = s(:irange, call.children[0], call.children[2])
          call = call.updated(nil, [s(:begin, range), :step, s(:int, -1)])
          process node.updated(nil, [call, *node.children[1..-1]])

        elsif call.children[1] == :upto
          range = s(:irange, call.children[0], call.children[2])
          call = call.updated(nil, [s(:begin, range), :step, s(:int, 1)])
          process node.updated(nil, [call, *node.children[1..-1]])

        elsif 
          call.children[1] == :each and call.children[0].type == :send and
          call.children[0].children[1] == :step
        then
          # i.step(j, n).each {|v| ...}
          range = call.children[0]
          step = range.children[3] || s(:int, 1)
          call = call.updated(nil, [s(:begin, 
            s(:irange, range.children[0], range.children[2])),
            :step, step])
          process node.updated(nil, [call, *node.children[1..-1]])

        else
          super
        end
      end

      def on_class(node)
        name, inheritance, *body = node.children
        body.compact!

        if inheritance == s(:const, nil, :Exception)
          unless 
            body.any? {|statement| statement.type == :def and
            statement.children.first == :initialize}
          then
            body.unshift S(:def, :initialize, s(:args, s(:arg, :message)),
              s(:begin, S(:send, s(:self), :message=, s(:lvar, :message)),
              S(:send, s(:self), :name=, s(:sym, name.children[1])),
              S(:send, s(:self), :stack=, s(:send, s(:send, nil, :Error,
              s(:lvar, :message)), :stack))))
          end

          body = [s(:begin, *body)] if body.length > 1
          S(:class, name, s(:const, nil, :Error), *body)
        else
          super
        end
      end
    end

    DEFAULTS.push Functions
  end
end