# frozen_string_literal: true

module Puppet::Pops
module Evaluator
  class Jumper < Exception # rubocop:disable Lint/InheritException
    attr_reader :value
    attr_reader :file
    attr_reader :line

    def initialize(value, file, line)
      @value = value
      @file = file
      @line = line
    end
  end

  class Next < Jumper; end

  class Return < Jumper; end

  class PuppetStopIteration < StopIteration
    attr_reader :file
    attr_reader :line
    attr_reader :pos

    def initialize(file, line, pos = nil)
      @file = file
      @line = line
      @pos = pos
    end

    def message
      "break() from context where this is illegal"
    end
  end

  # A Closure represents logic bound to a particular scope.
  # As long as the runtime (basically the scope implementation) has the behavior of Puppet 3x it is not
  # safe to return and later use this closure.
  #
  # The 3x scope is essentially a named scope with an additional internal local/ephemeral nested scope state.
  # In 3x there is no way to directly refer to the nested scopes, instead, the named scope must be in a particular
  # state. Specifically, closures that require a local/ephemeral scope to exist at a later point will fail.
  # It is safe to call a closure (even with 3x scope) from the very same place it was defined, but not
  # returning it and expecting the closure to reference the scope's state at the point it was created.
  #
  # Note that this class is a CallableSignature, and the methods defined there should be used
  # as the API for obtaining information in a callable-implementation agnostic way.
  #
  class Closure < CallableSignature
    attr_reader :evaluator
    attr_reader :model
    attr_reader :enclosing_scope

    def initialize(evaluator, model)
      @evaluator = evaluator
      @model = model
    end

    # Evaluates a closure in its enclosing scope after having matched given arguments with parameters (from left to right)
    # @api public
    def call(*args)
      call_with_scope(enclosing_scope, args)
    end

    # This method makes a Closure compatible with a Dispatch. This is used when the closure is wrapped in a Function
    # and the function is called. (Saves an extra Dispatch that just delegates to a Closure and avoids having two
    # checks of the argument type/arity validity).
    # @api private
    def invoke(instance, calling_scope, args, &block)
      enclosing_scope.with_global_scope do |global_scope|
        call_with_scope(global_scope, args, &block)
      end
    end

    def call_by_name_with_scope(scope, args_hash, enforce_parameters)
      call_by_name_internal(scope, args_hash, enforce_parameters)
    end

    def call_by_name(args_hash, enforce_parameters)
      call_by_name_internal(enclosing_scope, args_hash, enforce_parameters)
    end

    # Call closure with argument assignment by name
    def call_by_name_internal(closure_scope, args_hash, enforce_parameters)
      if enforce_parameters
        # Push a temporary parameter scope used while resolving the parameter defaults
        closure_scope.with_parameter_scope(closure_name, parameter_names) do |param_scope|
          # Assign all non-nil values, even those that represent non-existent parameters.
          args_hash.each { |k, v| param_scope[k] = v unless v.nil? }
          parameters.each do |p|
            name = p.name
            arg = args_hash[name]
            if arg.nil?
              # Arg either wasn't given, or it was undef
              if p.value.nil?
                # No default. Assign nil if the args_hash included it
                param_scope[name] = nil if args_hash.include?(name)
              else
                param_scope[name] = param_scope.evaluate(name, p.value, closure_scope, @evaluator)
              end
            end
          end
          args_hash = param_scope.to_hash
        end
        Types::TypeMismatchDescriber.validate_parameters(closure_name, params_struct, args_hash)
        result = catch(:next) do
          @evaluator.evaluate_block_with_bindings(closure_scope, args_hash, @model.body)
        end
        Types::TypeAsserter.assert_instance_of(nil, return_type, result) do
          "value returned from #{closure_name}"
        end
      else
        @evaluator.evaluate_block_with_bindings(closure_scope, args_hash, @model.body)
      end
    end
    private :call_by_name_internal

    def parameters
      @model.parameters
    end

    # Returns the number of parameters (required and optional)
    # @return [Integer] the total number of accepted parameters
    def parameter_count
      # yes, this is duplication of code, but it saves a method call
      @model.parameters.size
    end

    # @api public
    def parameter_names
      @model.parameters.collect(&:name)
    end

    def return_type
      @return_type ||= create_return_type
    end

    # @api public
    # rubocop:disable Naming/MemoizedInstanceVariableName
    def type
      @callable ||= create_callable_type
    end
    # rubocop:enable Naming/MemoizedInstanceVariableName

    # @api public
    def params_struct
      @params_struct ||= create_params_struct
    end

    # @api public
    def last_captures_rest?
      last = @model.parameters[-1]
      last && last.captures_rest
    end

    # @api public
    def block_name
      # TODO: Lambda's does not support blocks yet. This is a placeholder
      'unsupported_block'
    end

    CLOSURE_NAME = 'lambda'

    # @api public
    def closure_name
      CLOSURE_NAME
    end

    class Dynamic < Closure
      def initialize(evaluator, model, scope)
        @enclosing_scope = scope
        super(evaluator, model)
      end

      def enclosing_scope
        @enclosing_scope
      end

      def call(*args)
        # A return from an unnamed closure is treated as a return from the context evaluating
        # calling this closure - that is, as if it was the return call itself.
        #
        jumper = catch(:return) do
          return call_with_scope(enclosing_scope, args)
        end
        raise jumper
      end
    end

    class Named < Closure
      def initialize(name, evaluator, model)
        @name = name
        super(evaluator, model)
      end

      def closure_name
        @name
      end

      # The assigned enclosing scope, or global scope if enclosing scope was initialized to nil
      #
      def enclosing_scope
        # Named closures are typically used for puppet functions and they cannot be defined
        # in an enclosing scope as they are cashed and reused. They need to bind to the
        # global scope at time of use rather at time of definition.
        # Unnamed closures are always a runtime construct, they are never bound by a loader
        # and are thus garbage collected at end of a compilation.
        #
        Puppet.lookup(:global_scope) { {} }
      end
    end

    private

    def call_with_scope(scope, args)
      variable_bindings = combine_values_with_parameters(scope, args)

      final_args = parameters.reduce([]) do |tmp_args, param|
        if param.captures_rest
          tmp_args.concat(variable_bindings[param.name])
        else
          tmp_args << variable_bindings[param.name]
        end
      end

      if type.callable_with?(final_args, block_type)
        result = catch(:next) do
          @evaluator.evaluate_block_with_bindings(scope, variable_bindings, @model.body)
        end
        Types::TypeAsserter.assert_instance_of(nil, return_type, result) do
          "value returned from #{closure_name}"
        end
      else
        tc = Types::TypeCalculator.singleton
        args_type = tc.infer_set(final_args)
        raise ArgumentError, Types::TypeMismatchDescriber.describe_signatures(closure_name, [self], args_type)
      end
    end

    def combine_values_with_parameters(scope, args)
      scope.with_parameter_scope(closure_name, parameter_names) do |param_scope|
        parameters.each_with_index do |parameter, index|
          param_captures     = parameter.captures_rest
          default_expression = parameter.value

          if index >= args.size
            if default_expression
              # not given, has default
              value = param_scope.evaluate(parameter.name, default_expression, scope, @evaluator)

              if param_captures && !value.is_a?(Array)
                # correct non array default value
                value = [value]
              end
            elsif param_captures
              # not given, does not have default
              value = []
            # default for captures rest is an empty array
            else
              @evaluator.fail(Issues::MISSING_REQUIRED_PARAMETER, parameter, { :param_name => parameter.name })
            end
          else
            given_argument = args[index]
            if param_captures
              # get excess arguments
              value = args[(parameter_count - 1)..]
              # If the input was a single nil, or undef, and there is a default, use the default
              # This supports :undef in case it was used in a 3x data structure and it is passed as an arg
              #
              if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression
                value = param_scope.evaluate(parameter.name, default_expression, scope, @evaluator)
                # and ensure it is an array
                value = [value] unless value.is_a?(Array)
              end
            else
              value = given_argument
            end
          end
          param_scope[parameter.name] = value
        end
        param_scope.to_hash
      end
    end

    def create_callable_type
      types = []
      from = 0
      to = 0
      in_optional_parameters = false
      closure_scope = enclosing_scope

      parameters.each do |param|
        type, param_range = create_param_type(param, closure_scope)

        types << type

        if param_range[0] == 0
          in_optional_parameters = true
        elsif param_range[0] != 0 && in_optional_parameters
          @evaluator.fail(Issues::REQUIRED_PARAMETER_AFTER_OPTIONAL, param, { :param_name => param.name })
        end

        from += param_range[0]
        to += param_range[1]
      end
      param_types = Types::PTupleType.new(types, Types::PIntegerType.new(from, to))
      # The block_type for a Closure is always nil for now, see comment in block_name above
      Types::PCallableType.new(param_types, nil, return_type)
    end

    def create_params_struct
      type_factory = Types::TypeFactory
      members = {}
      closure_scope = enclosing_scope

      parameters.each do |param|
        arg_type, _ = create_param_type(param, closure_scope)
        key_type = type_factory.string(param.name.to_s)
        key_type = type_factory.optional(key_type) unless param.value.nil?
        members[key_type] = arg_type
      end
      type_factory.struct(members)
    end

    def create_return_type
      if @model.return_type
        @evaluator.evaluate(@model.return_type, @enclosing_scope)
      else
        Types::PAnyType::DEFAULT
      end
    end

    def create_param_type(param, closure_scope)
      type = if param.type_expr
               @evaluator.evaluate(param.type_expr, closure_scope)
             else
               Types::PAnyType::DEFAULT
             end

      if param.captures_rest && type.is_a?(Types::PArrayType)
        # An array on a slurp parameter is how a size range is defined for a
        # slurp (Array[Integer, 1, 3] *$param). However, the callable that is
        # created can't have the array in that position or else type checking
        # will require the parameters to be arrays, which isn't what is
        # intended. The array type contains the intended information and needs
        # to be unpacked.
        param_range = type.size_range
        type = type.element_type
      elsif param.captures_rest && !type.is_a?(Types::PArrayType)
        param_range = ANY_NUMBER_RANGE
      elsif param.value
        param_range = OPTIONAL_SINGLE_RANGE
      else
        param_range = REQUIRED_SINGLE_RANGE
      end
      [type, param_range]
    end

    # Produces information about parameters compatible with a 4x Function (which can have multiple signatures)
    def signatures
      [self]
    end

    ANY_NUMBER_RANGE = [0, Float::INFINITY]
    OPTIONAL_SINGLE_RANGE = [0, 1]
    REQUIRED_SINGLE_RANGE = [1, 1]
  end
end
end
