// Closed admission for ordinary prototype-data member calls whose receiver
// changes at every activation. The AST is lowered by meaning; source text,
// identifier spellings, and fixture names are not admission inputs.

///|
const CHANGING_RECEIVER_MAX_DEPTH = 4096.0

///|
priv struct ChangingReceiverActivationPlan {
  parameter : String
  next_property : String
  method_property : String
  base_condition_loc : @token.Loc
  recursive_add_loc : @token.Loc
  next_member_loc : @token.Loc
  method_member_loc : @token.Loc
  recursive_call_loc : @token.Loc
  recursive_subtract_loc : @token.Loc
}

///|
fn ChangingReceiverActivationPlan::ChangingReceiverActivationPlan(
  parameter~ : String,
  next_property~ : String,
  method_property~ : String,
  base_condition_loc~ : @token.Loc,
  recursive_add_loc~ : @token.Loc,
  next_member_loc~ : @token.Loc,
  method_member_loc~ : @token.Loc,
  recursive_call_loc~ : @token.Loc,
  recursive_subtract_loc~ : @token.Loc,
) -> ChangingReceiverActivationPlan {
  {
    parameter,
    next_property,
    method_property,
    base_condition_loc,
    recursive_add_loc,
    next_member_loc,
    method_member_loc,
    recursive_call_loc,
    recursive_subtract_loc,
  }
}

///|
fn lower_changing_receiver_activation(
  data : FuncData,
) -> ChangingReceiverActivationPlan? {
  guard data.params.length() == 1 && data.body.length() == 2 else {
    return None
  }
  let parameter = data.params[0]
  guard numeric_recursion_identifier_is_safe(parameter) else { return None }
  let base_condition_loc = match data.body[0] {
    @ast.IfStmt(
      @ast.Binary(
        @ast.EqEqEq,
        @ast.Ident(condition_parameter, _),
        base_value,
        condition_loc
      ),
      @ast.ReturnStmt(Some(return_value), _),
      None,
      _
    ) if condition_parameter == parameter &&
      exact_numeric_recursion_number(base_value, NUMERIC_RECURSION_BASE) &&
      exact_numeric_recursion_number(return_value, NUMERIC_RECURSION_BASE) =>
      condition_loc
    _ => return None
  }
  match data.body[1] {
    @ast.ReturnStmt(
      Some(
        @ast.Binary(
          @ast.Add,
          increment,
          @ast.Call(
            @ast.Member(
              @ast.Member(@ast.ThisExpr(_), next_property, next_member_loc),
              method_property,
              method_member_loc
            ),
            [
              @ast.Binary(
                @ast.Sub,
                @ast.Ident(argument_parameter, _),
                decrement,
                recursive_subtract_loc
              ),
            ],
            recursive_call_loc
          ),
          recursive_add_loc
        )
      ),
      _
    ) if argument_parameter == parameter &&
      next_property != "__proto__" &&
      method_property != "__proto__" &&
      exact_numeric_recursion_number(increment, NUMERIC_RECURSION_STEP) &&
      exact_numeric_recursion_number(decrement, NUMERIC_RECURSION_STEP) =>
      Some(
        ChangingReceiverActivationPlan(
          parameter~,
          next_property~,
          method_property~,
          base_condition_loc~,
          recursive_add_loc~,
          next_member_loc~,
          method_member_loc~,
          recursive_call_loc~,
          recursive_subtract_loc~,
        ),
      )
    _ => None
  }
}

///|
fn changing_receiver_plan_matches(
  expected : ChangingReceiverActivationPlan,
  actual : ChangingReceiverActivationPlan,
) -> Bool {
  expected.parameter == actual.parameter &&
  expected.next_property == actual.next_property &&
  expected.method_property == actual.method_property &&
  expected.base_condition_loc == actual.base_condition_loc &&
  expected.recursive_add_loc == actual.recursive_add_loc &&
  expected.next_member_loc == actual.next_member_loc &&
  expected.method_member_loc == actual.method_member_loc &&
  expected.recursive_call_loc == actual.recursive_call_loc &&
  expected.recursive_subtract_loc == actual.recursive_subtract_loc
}

///|
fn changing_receiver_value_identity(expected : Value, actual : Value) -> Bool {
  match (expected, actual) {
    (Object(expected_data), Object(actual_data)) =>
      physical_equal(expected_data, actual_data)
    _ => false
  }
}

///|
fn changing_receiver_is_ordinary(value : Value) -> Bool {
  match value {
    Object(data) =>
      data.class_name == "Object" &&
      data.callable is None &&
      data.arraybuffer_state is None &&
      data.bag.symbol_properties.is_empty() &&
      data.bag.symbol_descriptors.is_empty() &&
      data.bag.internal_slots.is_empty() &&
      data.bag.host_slots.is_empty()
    _ => false
  }
}

///|
fn changing_receiver_own_data_property(
  receiver : Value,
  property : String,
) -> Value? {
  guard changing_receiver_is_ordinary(receiver) && receiver is Object(data) else {
    return None
  }
  match
    (
      ordinary_get_own_string_desc(data.bag, property),
      data.bag.properties.get(property),
    ) {
    (Some(desc), Some(value)) if !desc.is_accessor => Some(value)
    _ => None
  }
}

///|
fn changing_receiver_resolves_prototype_data_method(
  receiver : Value,
  property : String,
  expected : Value,
) -> Bool {
  guard changing_receiver_is_ordinary(receiver) && receiver is Object(data) else {
    return false
  }
  guard ordinary_get_own_string_desc(data.bag, property) is None else {
    return false
  }
  let seen : Array[ObjectData] = []
  let mut current = data.prototype
  for _ in 0..<1024 {
    match current {
      Null => return false
      Object(prototype) => {
        guard prototype.class_name == "Object" &&
          prototype.callable is None &&
          prototype.arraybuffer_state is None else {
          return false
        }
        for prior in seen {
          guard !physical_equal(prior, prototype) else { return false }
        }
        seen.push(prototype)
        match
          (
            ordinary_get_own_string_desc(prototype.bag, property),
            prototype.bag.properties.get(property),
          ) {
          (Some(desc), Some(value)) if !desc.is_accessor =>
            return changing_receiver_value_identity(expected, value)
          (None, None) => current = prototype.prototype
          _ => return false
        }
      }
      _ => return false
    }
  }
  false
}

///|
priv struct ChangingReceiverFunctionSnapshot {
  plan : ChangingReceiverActivationPlan
  callee : Value
  name : String?
  params : Array[String]
  body : Array[@ast.Stmt]
  closure : Environment
  strict : Bool
  has_name_binding : Bool
  is_method : Bool
  source_text : String?
  expected_realm_protos : FunctionRealmProtos
  expected_source_identity : String?
}

///|
priv struct ChangingReceiverCallPreflight {
  function : ChangingReceiverFunctionSnapshot
  receivers : Array[Value]
  initial_depth : Int
  root_call_loc : @token.Loc
  throw_type_error_snapshot : CallbackFreeBindingSnapshot
  expected_global_object : ObjectData
}

///|
priv struct TrustedChangingReceiverRegistry {
  function : ChangingReceiverFunctionSnapshot
  receivers : Array[Value]
  initial_depth : Int
  root_call_loc : @token.Loc
  throw_type_error : Value?
  throw_type_error_snapshot : CallbackFreeBindingSnapshot
  expected_global_object : ObjectData
}

///|
fn changing_receiver_depth(args : Array[Value]) -> Int? {
  guard args.length() == 1 && args[0] is Number(value) else { return None }
  guard value >= 0.0 &&
    value <= CHANGING_RECEIVER_MAX_DEPTH &&
    value == value.floor() else {
    return None
  }
  Some(value.to_int())
}

///|
fn Interpreter::snapshot_changing_receiver_function(
  self : Interpreter,
  callee : Value,
) -> ChangingReceiverFunctionSnapshot? {
  ignore(self)
  guard callee is Object(object) &&
    object.class_name == "Function" &&
    object.callable is Some(UserFunc(data)) &&
    lower_changing_receiver_activation(data) is Some(plan) else {
    return None
  }
  let expected_realm_protos = callee_realm_protos(callee)
  let expected_source_identity = function_source_identity(callee)
  guard numeric_recursion_realm_protos_match(
      expected_realm_protos,
      callee_realm_protos(callee),
    ) &&
    function_source_identity(callee) == expected_source_identity else {
    return None
  }
  Some({
    plan,
    callee,
    name: data.name,
    params: data.params,
    body: data.body,
    closure: data.closure,
    strict: data.strict,
    has_name_binding: data.has_name_binding,
    is_method: data.is_method,
    source_text: data.source_text,
    expected_realm_protos,
    expected_source_identity,
  })
}

///|
fn Interpreter::changing_receiver_function_matches(
  self : Interpreter,
  snapshot : ChangingReceiverFunctionSnapshot,
) -> Bool {
  guard snapshot.callee is Object(expected) &&
    snapshot.callee is Object(actual) &&
    physical_equal(expected, actual) &&
    actual.class_name == "Function" &&
    actual.callable is Some(UserFunc(data)) &&
    lower_changing_receiver_activation(data) is Some(plan) else {
    return false
  }
  changing_receiver_plan_matches(snapshot.plan, plan) &&
  data.name == snapshot.name &&
  physical_equal(data.params, snapshot.params) &&
  physical_equal(data.body, snapshot.body) &&
  physical_equal(data.closure, snapshot.closure) &&
  data.strict == snapshot.strict &&
  data.has_name_binding == snapshot.has_name_binding &&
  data.is_method == snapshot.is_method &&
  data.source_text == snapshot.source_text &&
  numeric_recursion_realm_protos_match(
    snapshot.expected_realm_protos,
    callee_realm_protos(snapshot.callee),
  ) &&
  function_source_identity(snapshot.callee) == snapshot.expected_source_identity &&
  self.global.parent is None
}

///|
fn changing_receiver_chain(
  initial_receiver : Value,
  callee : Value,
  plan : ChangingReceiverActivationPlan,
  depth : Int,
) -> Array[Value]? {
  guard changing_receiver_resolves_prototype_data_method(
    initial_receiver,
    plan.method_property,
    callee,
  ) else {
    return None
  }
  let receivers = [initial_receiver]
  let mut current = initial_receiver
  for _ in 0.. value
      None => return None
    }
    guard changing_receiver_resolves_prototype_data_method(
      next,
      plan.method_property,
      callee,
    ) else {
      return None
    }
    receivers.push(next)
    current = next
  }
  Some(receivers)
}

///|
fn Interpreter::preflight_changing_receiver_call(
  self : Interpreter,
  callee : Value,
  this_value : Value,
  args : Array[Value],
  root_call_loc : @token.Loc,
) -> ChangingReceiverCallPreflight? {
  guard changing_receiver_depth(args) is Some(initial_depth) &&
    self.snapshot_changing_receiver_function(callee) is Some(function) &&
    self.numeric_recursion_canonical_global_object()
    is Some(expected_global_object) else {
    return None
  }
  let throw_type_error_snapshot = numeric_recursion_callback_free_throw_type_error(
    self.global,
  )
  match throw_type_error_snapshot {
    CallbackFreeBindingUnsafe => return None
    _ => ()
  }
  guard changing_receiver_chain(
      this_value,
      callee,
      function.plan,
      initial_depth,
    )
    is Some(receivers) else {
    return None
  }
  Some({
    function,
    receivers,
    initial_depth,
    root_call_loc,
    throw_type_error_snapshot,
    expected_global_object,
  })
}

///|
fn changing_receiver_preflights_match(
  expected : ChangingReceiverCallPreflight,
  actual : ChangingReceiverCallPreflight,
) -> Bool {
  guard expected.initial_depth == actual.initial_depth &&
    expected.root_call_loc == actual.root_call_loc &&
    changing_receiver_plan_matches(expected.function.plan, actual.function.plan) &&
    changing_receiver_value_identity(
      expected.function.callee,
      actual.function.callee,
    ) &&
    expected.receivers.length() == actual.receivers.length() else {
    return false
  }
  for index, receiver in expected.receivers {
    guard changing_receiver_value_identity(receiver, actual.receivers[index]) else {
      return false
    }
  }
  true
}

///|
fn Interpreter::seal_changing_receiver_registry(
  self : Interpreter,
  preflight : ChangingReceiverCallPreflight,
  callee : Value,
  this_value : Value,
  args : Array[Value],
  root_call_loc : @token.Loc,
) -> TrustedChangingReceiverRegistry raise InvalidActivationDispatchShell {
  guard self.preflight_changing_receiver_call(
      callee, this_value, args, root_call_loc,
    )
    is Some(actual) &&
    changing_receiver_preflights_match(preflight, actual) &&
    self.changing_receiver_function_matches(preflight.function) &&
    self.numeric_recursion_canonical_global_object() is Some(global_object) &&
    physical_equal(global_object, preflight.expected_global_object) else {
    invalid_activation_dispatch_shell(
      "changing-receiver call provenance changed before sealing",
    )
  }
  let throw_type_error_snapshot = numeric_recursion_callback_free_throw_type_error(
    self.global,
  )
  guard numeric_recursion_callback_free_binding_snapshots_match(
    preflight.throw_type_error_snapshot,
    throw_type_error_snapshot,
  ) else {
    invalid_activation_dispatch_shell(
      "changing-receiver ThrowTypeError binding changed before sealing",
    )
  }
  {
    function: preflight.function,
    receivers: preflight.receivers.copy(),
    initial_depth: preflight.initial_depth,
    root_call_loc: preflight.root_call_loc,
    throw_type_error: numeric_recursion_throw_type_error_value(
      throw_type_error_snapshot,
    ),
    throw_type_error_snapshot,
    expected_global_object: preflight.expected_global_object,
  }
}

///|
fn TrustedChangingReceiverRegistry::receiver_index(
  self : TrustedChangingReceiverRegistry,
  args : Array[Value],
) -> Int raise InvalidActivationDispatchShell {
  let depth = match changing_receiver_depth(args) {
    Some(value) => value
    None =>
      invalid_activation_dispatch_shell(
        "changing-receiver call argument escaped the admitted depth domain",
      )
  }
  guard depth <= self.initial_depth else {
    invalid_activation_dispatch_shell(
      "changing-receiver call depth exceeded the admitted root",
    )
  }
  self.initial_depth - depth
}

///|
fn TrustedChangingReceiverRegistry::require_call(
  self : TrustedChangingReceiverRegistry,
  interp : Interpreter,
  callee : Value,
  this_value : Value,
  args : Array[Value],
) -> Int raise InvalidActivationDispatchShell {
  let throw_type_error_snapshot = numeric_recursion_callback_free_throw_type_error(
    interp.global,
  )
  guard interp.numeric_recursion_canonical_global_object()
    is Some(global_object) &&
    physical_equal(global_object, self.expected_global_object) &&
    numeric_recursion_callback_free_binding_snapshots_match(
      self.throw_type_error_snapshot,
      throw_type_error_snapshot,
    ) &&
    interp.changing_receiver_function_matches(self.function) &&
    changing_receiver_value_identity(self.function.callee, callee) else {
    invalid_activation_dispatch_shell(
      "changing-receiver callable escaped sealed provenance",
    )
  }
  let index = self.receiver_index(args)
  guard index < self.receivers.length() &&
    changing_receiver_value_identity(self.receivers[index], this_value) else {
    invalid_activation_dispatch_shell(
      "changing-receiver activation lost receiver/depth correspondence",
    )
  }
  index
}

///|
fn TrustedChangingReceiverRegistry::next_receiver(
  self : TrustedChangingReceiverRegistry,
  interp : Interpreter,
  current : Value,
  args : Array[Value],
) -> Value raise InvalidActivationDispatchShell {
  let index = self.require_call(interp, self.function.callee, current, args)
  guard index + 1 < self.receivers.length() else {
    invalid_activation_dispatch_shell(
      "changing-receiver activation requested a successor past its base case",
    )
  }
  let next = self.receivers[index + 1]
  guard changing_receiver_own_data_property(
      current,
      self.function.plan.next_property,
    )
    is Some(actual_next) &&
    changing_receiver_value_identity(next, actual_next) &&
    changing_receiver_resolves_prototype_data_method(
      next,
      self.function.plan.method_property,
      self.function.callee,
    ) else {
    invalid_activation_dispatch_shell(
      "changing-receiver object graph changed after admission",
    )
  }
  next
}