# frozen_string_literal: true

require 'opal/rewriters/base'
require 'opal/rewriters/arguments'

module Opal
  module Rewriters
    # Converts
    #
    #   def m( a, b = 1, *c, d, e:, f: 1, **g, &blk )
    #   end
    #
    # To something like
    #
    #   def m( a, <fake b>, <fake c>, <fake d>, <fake kwargs>)
    #     blk = <extract block>
    #     $post_args = arguments[1..-1]
    #     $kwargs = $post_args.pop
    #     a = <enough args> ? $post_args.shift : 1
    #     c = <enough args> ? $post_args[0..-1] : []
    #     d = $post_args.last
    #     e = $kwargs.delete(:e)
    #     f = $kwargs.delete(:f) || 1
    #     g = $kwargs.except(:e, :f)
    #   end
    #
    class InlineArgs < Base
      def on_def(node)
        node = super(node)
        mid, args, body = *node

        body ||= s(:nil) # prevent returning initialization statement

        initializer = Initializer.new(args, type: :def)
        inline_args = args.updated(nil, initializer.inline)
        body = prepend_to_body(body, initializer.initialization)

        node.updated(nil, [mid, inline_args, body])
      end

      def on_defs(node)
        node = super(node)
        recv, mid, args, body = *node

        body ||= s(:nil) # prevent returning initialization statement

        initializer = Initializer.new(args, type: :defs)
        inline_args = args.updated(nil, initializer.inline)
        body = prepend_to_body(body, initializer.initialization)

        node.updated(nil, [recv, mid, inline_args, body])
      end

      def on_iter(node)
        node = super(node)
        args, body = *node

        body ||= s(:nil) # prevent returning initialization statement

        initializer = Initializer.new(args, type: :iter)
        inline_args = args.updated(nil, initializer.inline)
        body = prepend_to_body(body, initializer.initialization)

        node.updated(nil, [inline_args, body])
      end

      class Initializer < ::Opal::Rewriters::Base
        attr_reader :inline, :initialization

        STEPS = %i[
          extract_blockarg
          initialize_shadowargs
          extract_args

          prepare_post_args
          prepare_kwargs

          extract_optargs
          extract_restarg
          extract_post_args

          extract_kwargs
          extract_kwoptargs
          extract_kwrestarg
        ].freeze

        def initialize(args, type:)
          @args = Arguments.new(args.children)

          @inline = []
          @initialization = []

          @type = type

          STEPS.each do |step|
            send(step)
          end

          if @initialization.any?
            @initialization = s(:begin, *@initialization)
          else
            @initialization = nil
          end
        end

        def extract_blockarg
          if (arg = @args.blockarg)
            @initialization << arg.updated(:extract_blockarg)
          end
        end

        def initialize_shadowargs
          @args.shadowargs.each do |arg|
            @initialization << arg.updated(:initialize_shadowarg)
          end
        end

        def extract_args
          @args.args.each do |arg|
            if @type == :iter
              # block args are not required,
              # so we need to tell compiler that required args
              # must be initialized with nil-s
              @initialization << arg.updated(:initialize_iter_arg)
            else
              # required inline def argument like 'def m(req)'
              # no initialization is required
            end
            @inline << arg
          end
        end

        def prepare_post_args
          if @args.has_post_args?
            @initialization << s(:prepare_post_args, @args.args.length)
          end
        end

        def prepare_kwargs
          return unless @args.has_any_kwargs?

          if @args.can_inline_kwargs?
            @inline << s(:arg, :'$kwargs')
          else
            @initialization << s(:extract_kwargs)
            @inline << s(:fake_arg)
          end

          @initialization << s(:ensure_kwargs_are_kwargs)
        end

        def extract_kwargs
          @args.kwargs.each do |arg|
            @initialization << arg.updated(:extract_kwarg)
          end
        end

        def extract_kwoptargs
          @args.kwoptargs.each do |arg|
            @initialization << arg.updated(:extract_kwoptarg)
          end
        end

        def extract_kwrestarg
          if (arg = @args.kwrestarg)
            @initialization << arg.updated(:extract_kwrestarg)
          end
        end

        def extract_post_args
          # post arguments must be extracted with an offset
          @args.postargs.each do |arg|
            @initialization << arg.updated(:extract_post_arg)
            @inline << s(:fake_arg)
          end
        end

        def extract_optargs
          has_post_args = @args.has_post_args?
          @args.optargs.each do |arg|
            if has_post_args
              # optional post argument like 'def m(opt = 1, a)'
              arg_name, default_value = *arg
              @initialization << arg.updated(:extract_post_optarg, [arg_name, default_value, args_to_keep])
              @inline << s(:fake_arg)
            else
              # optional inline argument like 'def m(a, opt = 1)'
              @inline << arg.updated(:arg)
              @initialization << arg.updated(:extract_optarg)
            end
          end
        end

        def extract_restarg
          if (arg = @args.restarg)
            arg_name = arg.children[0]
            @initialization << arg.updated(:extract_restarg, [arg_name, args_to_keep])
            @inline << s(:fake_arg)
          end
        end

        def args_to_keep
          @args.postargs.length
        end
      end
    end
  end
end
