// Exact, callback-free admission for the #616 Proxy `get` recursion slice.
// The normalized plan contains syntax only; runtime provenance is sealed after
// the admitted root setup has created the Proxy and its trap function.

///|
const PROXY_GET_RECURSION_CONSTRUCTOR_NAME = "Proxy"

///|
const PROXY_GET_RECURSION_TRAP_NAME = "get"

///|
#warnings("-unused_field")
priv struct ProxyGetRecursionExpressionRecipe {
  base_condition_loc : @token.Loc
  decrement_loc : @token.Loc
  recursive_add_loc : @token.Loc
  recursive_member_loc : @token.Loc
  root_member_loc : @token.Loc
}

///|
fn ProxyGetRecursionExpressionRecipe::ProxyGetRecursionExpressionRecipe(
  base_condition_loc~ : @token.Loc,
  decrement_loc~ : @token.Loc,
  recursive_add_loc~ : @token.Loc,
  recursive_member_loc~ : @token.Loc,
  root_member_loc~ : @token.Loc,
) -> ProxyGetRecursionExpressionRecipe {
  {
    base_condition_loc,
    decrement_loc,
    recursive_add_loc,
    recursive_member_loc,
    root_member_loc,
  }
}

///|
#warnings("-unused_field")
priv struct ProxyGetRecursionPlan {
  counter_name : String
  proxy_name : String
  property_name : String
  target_parameter : String
  key_parameter : String
  receiver_parameter : String
  initial_count : Double
  recipe : ProxyGetRecursionExpressionRecipe
}

///|
fn ProxyGetRecursionPlan::ProxyGetRecursionPlan(
  counter_name~ : String,
  proxy_name~ : String,
  property_name~ : String,
  target_parameter~ : String,
  key_parameter~ : String,
  receiver_parameter~ : String,
  initial_count~ : Double,
  recipe~ : ProxyGetRecursionExpressionRecipe,
) -> ProxyGetRecursionPlan {
  {
    counter_name,
    proxy_name,
    property_name,
    target_parameter,
    key_parameter,
    receiver_parameter,
    initial_count,
    recipe,
  }
}

///|
fn proxy_get_recursion_static_property_name(expr : @ast.Expr) -> String? {
  match expr {
    @ast.StringLit(name, false, @token.LexForm::LexNormal, _) => Some(name)
    _ => None
  }
}

///|
fn proxy_get_recursion_parameters_are_exact(
  params : Array[String],
  counter_name : String,
  proxy_name : String,
) -> Bool {
  guard params.length() == 3 else { return false }
  let target_parameter = params[0]
  let key_parameter = params[1]
  let receiver_parameter = params[2]
  numeric_recursion_identifier_is_safe(target_parameter) &&
  numeric_recursion_identifier_is_safe(key_parameter) &&
  numeric_recursion_identifier_is_safe(receiver_parameter) &&
  target_parameter != key_parameter &&
  target_parameter != receiver_parameter &&
  key_parameter != receiver_parameter &&
  target_parameter != counter_name &&
  target_parameter != proxy_name &&
  key_parameter != counter_name &&
  key_parameter != proxy_name &&
  receiver_parameter != counter_name &&
  receiver_parameter != proxy_name
}

///|
fn classify_proxy_get_recursion_body(
  body : Array[@ast.Stmt],
  counter_name : String,
  proxy_name : String,
  property_name : String,
  root_member_loc : @token.Loc,
) -> ProxyGetRecursionExpressionRecipe? {
  guard body.length() == 3 else { return None }
  let base_condition_loc = match body[0] {
    @ast.IfStmt(
      @ast.Binary(
        @ast.EqEqEq,
        @ast.Ident(condition_counter, _),
        base_condition,
        condition_loc
      ),
      @ast.ReturnStmt(Some(base_result), _),
      None,
      _
    ) if condition_counter == counter_name &&
      exact_numeric_recursion_number(base_condition, NUMERIC_RECURSION_BASE) &&
      exact_numeric_recursion_number(base_result, NUMERIC_RECURSION_BASE) =>
      condition_loc
    _ => return None
  }
  let decrement_loc = match body[1] {
    @ast.ExprStmt(
      @ast.Assign(
        assignment_counter,
        @ast.Binary(
          @ast.Sub,
          @ast.Ident(decrement_counter, _),
          decrement,
          subtraction_loc
        ),
        _
      ),
      _
    ) if assignment_counter == counter_name &&
      decrement_counter == counter_name &&
      exact_numeric_recursion_number(decrement, NUMERIC_RECURSION_STEP) =>
      subtraction_loc
    _ => return None
  }
  let (recursive_add_loc, recursive_member_loc) = match body[2] {
    @ast.ReturnStmt(
      Some(
        @ast.Binary(
          @ast.Add,
          increment,
          @ast.Member(
            @ast.Ident(recursive_proxy, _),
            recursive_property,
            member_loc
          ),
          add_loc
        )
      ),
      _
    ) if recursive_proxy == proxy_name &&
      recursive_property == property_name &&
      exact_numeric_recursion_number(increment, NUMERIC_RECURSION_STEP) =>
      (add_loc, member_loc)
    _ => return None
  }
  Some(
    ProxyGetRecursionExpressionRecipe(
      base_condition_loc~,
      decrement_loc~,
      recursive_add_loc~,
      recursive_member_loc~,
      root_member_loc~,
    ),
  )
}

///|
fn classify_proxy_get_recursion_handler(
  expr : @ast.Expr,
  counter_name : String,
  proxy_name : String,
  property_name : String,
  root_member_loc : @token.Loc,
) -> (Array[String], ProxyGetRecursionExpressionRecipe)? {
  guard expr is @ast.ObjectLit(properties, _) && properties.length() == 1 else {
    return None
  }
  let property = properties[0]
  guard property.kind == @ast.Init &&
    !property.computed &&
    property.is_method &&
    proxy_get_recursion_static_property_name(property.key) ==
    Some(PROXY_GET_RECURSION_TRAP_NAME) else {
    return None
  }
  match property.value {
    @ast.FuncExpr(Some(trap_name), params, body, _, _) if trap_name ==
      PROXY_GET_RECURSION_TRAP_NAME &&
      proxy_get_recursion_parameters_are_exact(params, counter_name, proxy_name) =>
      match
        classify_proxy_get_recursion_body(
          body, counter_name, proxy_name, property_name, root_member_loc,
        ) {
        Some(recipe) => Some((params, recipe))
        None => None
      }
    _ => None
  }
}

///|
fn classify_proxy_get_recursion_setup(
  stmt : @ast.Stmt,
  counter_name : String,
  proxy_name : String,
  property_name : String,
  root_member_loc : @token.Loc,
) -> (Array[String], ProxyGetRecursionExpressionRecipe)? {
  match stmt {
    @ast.ExprStmt(
      @ast.Assign(
        assignment_name,
        @ast.NewExpr(@ast.Ident(constructor_name, _), args, _),
        _
      ),
      _
    ) if assignment_name == proxy_name &&
      constructor_name == PROXY_GET_RECURSION_CONSTRUCTOR_NAME &&
      args.length() == 2 => {
      guard args[0] is @ast.ObjectLit(target_properties, _) &&
        target_properties.is_empty() else {
        return None
      }
      classify_proxy_get_recursion_handler(
        args[1],
        counter_name,
        proxy_name,
        property_name,
        root_member_loc,
      )
    }
    _ => None
  }
}

///|
#warnings("-unused_value")
fn classify_proxy_get_recursion_program(
  stmts : Array[@ast.Stmt],
) -> ProxyGetRecursionPlan? {
  guard stmts.length() == 4 else { return None }
  let (counter_name, initial_count) = match stmts[0] {
    @ast.VarDecl(@ast.LetKind, name, Some(initial), _) if numeric_recursion_identifier_is_safe(
        name,
      ) &&
      exact_numeric_recursion_number(
        initial,
        NUMERIC_RECURSION_INITIAL_ARGUMENT,
      ) &&
      name != PROXY_GET_RECURSION_CONSTRUCTOR_NAME =>
      (name, NUMERIC_RECURSION_INITIAL_ARGUMENT)
    _ => return None
  }
  let proxy_name = match stmts[1] {
    @ast.VarDecl(@ast.LetKind, name, None, _) if numeric_recursion_identifier_is_safe(
        name,
      ) &&
      name != counter_name &&
      name != PROXY_GET_RECURSION_CONSTRUCTOR_NAME => name
    _ => return None
  }
  let (root_proxy_name, property_name, root_member_loc) = match stmts[3] {
    @ast.ExprStmt(@ast.Member(@ast.Ident(name, _), property, member_loc), _) =>
      (name, property, member_loc)
    _ => return None
  }
  guard root_proxy_name == proxy_name else { return None }
  match
    classify_proxy_get_recursion_setup(
      stmts[2],
      counter_name,
      proxy_name,
      property_name,
      root_member_loc,
    ) {
    Some((params, recipe)) =>
      Some(
        ProxyGetRecursionPlan(
          counter_name~,
          proxy_name~,
          property_name~,
          target_parameter=params[0],
          key_parameter=params[1],
          receiver_parameter=params[2],
          initial_count~,
          recipe~,
        ),
      )
    None => None
  }
}

///|
#warnings("-unused_value")
fn proxy_get_recursion_plan_is_dispatchable(
  _plan : ProxyGetRecursionPlan,
) -> Bool {
  true
}

///|
fn proxy_get_recursion_seal_statement_index(
  _plan : ProxyGetRecursionPlan,
) -> Int {
  2
}

///|
fn proxy_get_recursion_root_statement_index(
  _plan : ProxyGetRecursionPlan,
) -> Int {
  3
}

///|
#warnings("-unused_value")
fn proxy_get_recursion_root_statement_matches(
  plan : ProxyGetRecursionPlan,
  index : Int,
  stmt : @ast.Stmt,
) -> Bool {
  match index {
    0 =>
      match stmt {
        @ast.VarDecl(@ast.LetKind, name, Some(initial), _) =>
          name == plan.counter_name &&
          exact_numeric_recursion_number(initial, plan.initial_count)
        _ => false
      }
    1 =>
      match stmt {
        @ast.VarDecl(@ast.LetKind, name, None, _) => name == plan.proxy_name
        _ => false
      }
    seal_index if seal_index == proxy_get_recursion_seal_statement_index(plan) =>
      match
        classify_proxy_get_recursion_setup(
          stmt,
          plan.counter_name,
          plan.proxy_name,
          plan.property_name,
          plan.recipe.root_member_loc,
        ) {
        Some((params, _)) =>
          params[0] == plan.target_parameter &&
          params[1] == plan.key_parameter &&
          params[2] == plan.receiver_parameter
        None => false
      }
    root_index if root_index == proxy_get_recursion_root_statement_index(plan) =>
      match stmt {
        @ast.ExprStmt(@ast.Member(@ast.Ident(name, _), property, _), _) =>
          name == plan.proxy_name && property == plan.property_name
        _ => false
      }
    _ => false
  }
}

///|
#warnings("-unused_value")
fn proxy_get_recursion_body_statement_matches(
  plan : ProxyGetRecursionPlan,
  index : Int,
  stmt : @ast.Stmt,
) -> Bool {
  match index {
    0 =>
      match stmt {
        @ast.IfStmt(
          @ast.Binary(
            @ast.EqEqEq,
            @ast.Ident(counter_name, _),
            base_condition,
            _
          ),
          @ast.ReturnStmt(Some(base_result), _),
          None,
          _
        ) =>
          counter_name == plan.counter_name &&
          exact_numeric_recursion_number(base_condition, NUMERIC_RECURSION_BASE) &&
          exact_numeric_recursion_number(base_result, NUMERIC_RECURSION_BASE)
        _ => false
      }
    1 =>
      match stmt {
        @ast.ExprStmt(
          @ast.Assign(
            assignment_counter,
            @ast.Binary(
              @ast.Sub,
              @ast.Ident(decrement_counter, _),
              decrement,
              _
            ),
            _
          ),
          _
        ) =>
          assignment_counter == plan.counter_name &&
          decrement_counter == plan.counter_name &&
          exact_numeric_recursion_number(decrement, NUMERIC_RECURSION_STEP)
        _ => false
      }
    2 =>
      match stmt {
        @ast.ReturnStmt(
          Some(
            @ast.Binary(
              @ast.Add,
              increment,
              @ast.Member(@ast.Ident(proxy_name, _), property_name, _),
              _
            )
          ),
          _
        ) =>
          proxy_name == plan.proxy_name &&
          property_name == plan.property_name &&
          exact_numeric_recursion_number(increment, NUMERIC_RECURSION_STEP)
        _ => false
      }
    _ => false
  }
}