# frozen_string_literal: true

require 'opal/rewriters/base'

module Opal
  module Rewriters
    class ReturnableLogic < Base
      def next_tmp
        @counter ||= 0
        @counter += 1
        "$ret_or_#{@counter}"
      end

      def free_tmp
        @counter -= 1
      end

      def reset_tmp_counter!
        @counter = nil
      end

      def on_if(node)
        test, = *node.children

        check_control_flow!(test)

        # The if_test metadata signifies that we don't care about the return value except if it's
        # truthy or falsy. And those tests will be carried out by the respective $truthy helper calls.
        test.meta[:if_test] = true if test
        super
      end

      def on_case(node)
        lhs, *whens, els = *node.children
        els ||= s(:nil)
        lhs_tmp = next_tmp if lhs

        out = build_if_from_when(node, lhs, lhs_tmp, whens, els)
        free_tmp if lhs
        out
      end

      # `a || b` / `a or b`
      def on_or(node)
        lhs, rhs = *node.children

        check_control_flow!(lhs)

        if node.meta[:if_test]
          # Let's forward the if_test to the lhs and rhs - since we don't care about the exact return
          # value of our or, we neither do care about a return value of our lhs or rhs.
          lhs.meta[:if_test] = rhs.meta[:if_test] = true
          out = process(node.updated(:if, [lhs, s(:true), rhs]))
        else
          lhs_tmp = next_tmp
          out = process(node.updated(:if, [s(:lvasgn, lhs_tmp, lhs), s(:js_tmp, lhs_tmp), rhs]))
          free_tmp
        end
        out
      end

      # `a && b` / `a and b`
      def on_and(node)
        lhs, rhs = *node.children

        check_control_flow!(lhs)

        if node.meta[:if_test]
          lhs.meta[:if_test] = rhs.meta[:if_test] = true
          out = process(node.updated(:if, [lhs, rhs, s(:false)]))
        else
          lhs_tmp = next_tmp
          out = process(node.updated(:if, [s(:lvasgn, lhs_tmp, lhs), rhs, s(:js_tmp, lhs_tmp)]))
          free_tmp
        end
        out
      end

      # Parser sometimes generates parentheses as a begin node. If it's a single node begin value, then
      # let's forward the if_test metadata.
      def on_begin(node)
        if node.meta[:if_test] && node.children.count == 1
          node.children.first.meta[:if_test] = true
        end
        node.meta.delete(:if_test)
        super
      end

      private

      def check_control_flow!(node)
        case node.type
        when :break, :next, :redo, :retry, :return
          error 'void value expression'
        end
      end

      def build_if_from_when(node, lhs, lhs_tmp, whens, els)
        first_when, *next_whens = *whens

        *parts, expr = *first_when.children

        rule = build_rule_from_parts(node, lhs, lhs_tmp, parts)

        first_when.updated(:if, [rule, process(expr), next_whens.empty? ? process(els) : build_if_from_when(nil, nil, lhs_tmp, next_whens, els)])
      end

      def build_rule_from_parts(node, lhs, lhs_tmp, parts)
        lhs = if node && lhs_tmp
                node.updated(:lvasgn, [lhs_tmp, process(lhs)])
              else
                s(:js_tmp, lhs_tmp)
              end

        first_part, *next_parts = *parts

        subrule = if first_part.type == :splat
                    splat_on = first_part.children.first
                    iter_val = next_tmp
                    block = s(:send, process(splat_on), :any?,
                      s(:iter,
                        s(:args, s(:arg, iter_val)),
                        build_rule_from_parts(nil, nil, lhs_tmp, [s(:lvar, iter_val)])
                      )
                    )
                    if node && lhs_tmp
                      s(:begin, lhs, block)
                    else
                      block
                    end
                  elsif lhs_tmp
                    s(:send, process(first_part), :===, lhs)
                  else
                    process(first_part)
                  end

        if next_parts.empty?
          subrule
        else
          s(:if, subrule, s(:true), build_rule_from_parts(nil, nil, lhs_tmp, next_parts))
        end
      end
    end
  end
end
