///|
pub fn compound_assign_binary_op(op : @ast.CompoundOp) -> @ast.BinOp? {
  match op {
    AddAssign => Some(Add)
    SubAssign => Some(Sub)
    MulAssign => Some(Mul)
    ExpAssign => Some(Exp)
    DivAssign => Some(Div)
    ModAssign => Some(Mod)
    BitAndAssign => Some(BitAnd)
    BitOrAssign => Some(BitOr)
    BitXorAssign => Some(BitXor)
    LShiftAssign => Some(LShift)
    RShiftAssign => Some(RShift)
    URShiftAssign => Some(URShift)
    LogicalAndAssign | LogicalOrAssign | NullishAssign => None
  }
}

///|
pub fn get_assignment_name_value(
  ctx : ExecContext,
  env : Environment,
  name : String,
) -> Value raise Error {
  @static_semantics.validate_strict_assignment_target_name(ctx.strict, name)
  env.get(name)
}

///|
pub fn Interpreter::eval_update_property(
  self : Interpreter,
  ctx : ExecContext,
  obj : Value,
  prop : String,
  op : @ast.UpdateOp,
  prefix : Bool,
  member_loc : @token.Loc,
  loc : @token.Loc,
) -> Value raise Error {
  let current = self.get_property(obj, prop, member_loc)
  let num = self.to_number(current)
  let new_val : Value = match op {
    Increment => Number(num + 1.0)
    Decrement => Number(num - 1.0)
  }
  let _ = self.set_property(obj, prop, new_val, loc, strict=ctx.strict)
  if prefix {
    new_val
  } else {
    Number(num)
  }
}

///|
pub fn Interpreter::eval_update_computed_property(
  self : Interpreter,
  ctx : ExecContext,
  obj : Value,
  key : Value,
  op : @ast.UpdateOp,
  prefix : Bool,
  member_loc : @token.Loc,
  loc : @token.Loc,
) -> Value raise Error {
  let prop_key = to_property_key(key, interp=Some(self))
  let current = self.get_computed_property(obj, prop_key, member_loc)
  let num = self.to_number(current)
  let new_val : Value = match op {
    Increment => Number(num + 1.0)
    Decrement => Number(num - 1.0)
  }
  let _ = self.set_computed_property(
    obj,
    prop_key,
    new_val,
    loc,
    strict=ctx.strict,
  )
  if prefix {
    new_val
  } else {
    Number(num)
  }
}

///|
fn unwrap_groupings(expr : @ast.Expr) -> @ast.Expr {
  let mut current = expr
  for ;; {
    match current {
      Grouping(inner, _) => current = inner
      _ => return current
    }
  }
}

///|
fn Interpreter::eval_update(
  self : Interpreter,
  ctx : ExecContext,
  op : @ast.UpdateOp,
  operand : @ast.Expr,
  prefix : Bool,
  env : Environment,
  loc : @token.Loc,
) -> Value raise Error {
  match operand {
    Ident(name, _) => {
      let current = get_assignment_name_value(ctx, env, name)
      let num = self.to_number(current)
      let new_val : Value = match op {
        Increment => Number(num + 1.0)
        Decrement => Number(num - 1.0)
      }
      if self.is_immutable_global(name) {
        if ctx.strict {
          raise @errors.TypeError(
            message="Cannot assign to read only property '\{name}' of object '[object global]'",
          )
        }
      } else {
        env.assign_with_strict(name, new_val, ctx.strict)
      }
      if prefix {
        new_val
      } else {
        Number(num)
      }
    }
    Member(obj_expr, prop, mloc) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      self.eval_update_property(ctx, obj, prop, op, prefix, mloc, loc)
    }
    ComputedMember(obj_expr, key_expr, mloc) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let key = self.eval_expr(ctx, key_expr, env)
      self.eval_update_computed_property(ctx, obj, key, op, prefix, mloc, loc)
    }
    SuperMember(prop, mloc) => {
      let this_val = eval_this_value(env)
      let super_proto = self.require_super_target(env, "super.prop", mloc)
      let current = self.get_property_from_prototype(
        this_val, super_proto, prop, mloc,
      )
      let num = self.to_number(current)
      let new_val : Value = match op {
        Increment => Number(num + 1.0)
        Decrement => Number(num - 1.0)
      }
      let _ = self.set_property(
        super_proto,
        prop,
        new_val,
        loc,
        strict=ctx.strict,
        receiver=this_val,
      )
      if prefix {
        new_val
      } else {
        Number(num)
      }
    }
    SuperComputedMember(key_expr, mloc) => {
      let this_val = eval_this_value(env)
      let key = self.eval_expr(ctx, key_expr, env)
      let super_proto = self.require_super_target(env, "super[expr]", mloc)
      let prop_key = to_property_key(key, interp=Some(self))
      let current = self.get_computed_property_from_prototype(
        this_val, super_proto, prop_key, mloc,
      )
      let num = self.to_number(current)
      let new_val : Value = match op {
        Increment => Number(num + 1.0)
        Decrement => Number(num - 1.0)
      }
      let _ = self.set_computed_property(
        super_proto,
        prop_key,
        new_val,
        loc,
        strict=ctx.strict,
        receiver=this_val,
      )
      if prefix {
        new_val
      } else {
        Number(num)
      }
    }
    PrivateMember(obj_expr, name, _) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let current = get_private_member(obj, name, env)
      let num = self.to_number(current)
      let new_val : Value = match op {
        Increment => Number(num + 1.0)
        Decrement => Number(num - 1.0)
      }
      let brand = resolve_private_brand(env, name)
      set_private_field(obj, brand, name, new_val)
      if prefix {
        new_val
      } else {
        Number(num)
      }
    }
    Call(_, _, _) => {
      if ctx.strict {
        raise @errors.SyntaxError(
          message="Invalid update target at line \{loc.line}, col \{loc.col}",
        )
      }
      let _ = self.eval_expr(ctx, operand, env)
      raise @errors.ReferenceError(
        message="Invalid left-hand side expression in prefix/postfix operation",
      )
    }
    Grouping(_, _) => {
      let inner = unwrap_groupings(operand)
      match inner {
        Call(_, _, _) => {
          if ctx.strict {
            raise @errors.SyntaxError(
              message="Invalid update target at line \{loc.line}, col \{loc.col}",
            )
          }
          let _ = self.eval_expr(ctx, inner, env)
          raise @errors.ReferenceError(
            message="Invalid left-hand side expression in prefix/postfix operation",
          )
        }
        _ =>
          raise @errors.SyntaxError(
            message="Invalid update target at line \{loc.line}, col \{loc.col}",
          )
      }
    }
    _ =>
      raise @errors.SyntaxError(
        message="Invalid update target at line \{loc.line}, col \{loc.col}",
      )
  }
}

///|
fn Interpreter::eval_compound_assign(
  self : Interpreter,
  ctx : ExecContext,
  op : @ast.CompoundOp,
  target : @ast.Expr,
  value_expr : @ast.Expr,
  env : Environment,
  loc : @token.Loc,
) -> Value raise Error {
  // Handle logical assignment operators with short-circuit semantics
  match op {
    LogicalAndAssign =>
      return self.eval_logical_assign(
        ctx, target, value_expr, env, loc, is_truthy,
      )
    LogicalOrAssign =>
      return self.eval_logical_assign(ctx, target, value_expr, env, loc, v => {
        !is_truthy(v)
      })
    NullishAssign =>
      return self.eval_logical_assign(ctx, target, value_expr, env, loc, v => {
        v is (Null | Undefined)
      })
    _ => ()
  }
  let bin_op : @ast.BinOp = match compound_assign_binary_op(op) {
    Some(bin_op) => bin_op
    None =>
      raise @errors.InternalError(
        message="unexpected logical compound assignment mapping",
      )
  }
  match target {
    Ident(name, _) => {
      let current = get_assignment_name_value(ctx, env, name)
      let rhs = self.eval_expr(ctx, value_expr, env)
      let result = eval_binary_op(bin_op, current, rhs, loc, interp=Some(self))
      // Check if the target is a non-writable property on the global object
      if self.is_immutable_global(name) {
        if ctx.strict {
          raise @errors.TypeError(
            message="Cannot assign to read only property '\{name}' of object '[object global]'",
          )
        }
        return result
      }
      env.assign_with_strict(name, result, ctx.strict)
      result
    }
    Member(obj_expr, prop, mloc) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let current = self.get_property(obj, prop, mloc)
      let rhs = self.eval_expr(ctx, value_expr, env)
      let result = eval_binary_op(bin_op, current, rhs, loc, interp=Some(self))
      let _ = self.set_property(obj, prop, result, loc, strict=ctx.strict)
      result
    }
    ComputedMember(obj_expr, key_expr, mloc) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let key = self.eval_expr(ctx, key_expr, env)
      // Per ES5 11.2.1 step 5: CheckObjectCoercible(baseValue) before ToString
      if obj is Null {
        raise @errors.TypeError(message="Cannot read properties of null")
      }
      if obj is Undefined {
        raise @errors.TypeError(message="Cannot read properties of undefined")
      }
      let prop_key = to_property_key(key, interp=Some(self))
      let current = self.get_computed_property(obj, prop_key, mloc)
      let rhs = self.eval_expr(ctx, value_expr, env)
      let result = eval_binary_op(bin_op, current, rhs, loc, interp=Some(self))
      let _ = self.set_computed_property(
        obj,
        prop_key,
        result,
        loc,
        strict=ctx.strict,
      )
      result
    }
    SuperMember(prop, mloc) => {
      let this_val = eval_this_value(env)
      let super_proto = self.require_super_target(env, "super.prop", mloc)
      let current = self.get_property_from_prototype(
        this_val, super_proto, prop, mloc,
      )
      let rhs = self.eval_expr(ctx, value_expr, env)
      let result = eval_binary_op(bin_op, current, rhs, loc, interp=Some(self))
      let _ = self.set_property(
        super_proto,
        prop,
        result,
        loc,
        strict=ctx.strict,
        receiver=this_val,
      )
      result
    }
    SuperComputedMember(key_expr, mloc) => {
      let this_val = eval_this_value(env)
      let key = self.eval_expr(ctx, key_expr, env)
      let super_proto = self.require_super_target(env, "super[expr]", mloc)
      let prop_key = to_property_key(key, interp=Some(self))
      let current = self.get_computed_property_from_prototype(
        this_val, super_proto, prop_key, mloc,
      )
      let rhs = self.eval_expr(ctx, value_expr, env)
      let result = eval_binary_op(bin_op, current, rhs, loc, interp=Some(self))
      let _ = self.set_computed_property(
        super_proto,
        prop_key,
        result,
        loc,
        strict=ctx.strict,
        receiver=this_val,
      )
      result
    }
    PrivateMember(obj_expr, name, _) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let current = get_private_member(obj, name, env)
      let rhs = self.eval_expr(ctx, value_expr, env)
      let result = eval_binary_op(bin_op, current, rhs, loc, interp=Some(self))
      let brand = resolve_private_brand(env, name)
      set_private_field(obj, brand, name, result)
      result
    }
    Call(_, _, _) => {
      if ctx.strict {
        raise @errors.SyntaxError(
          message="Invalid compound assignment target at line \{loc.line}, col \{loc.col}",
        )
      }
      let _ = self.eval_expr(ctx, target, env)
      raise @errors.ReferenceError(
        message="Invalid left-hand side in assignment",
      )
    }
    Grouping(_, _) => {
      let inner = unwrap_groupings(target)
      match inner {
        Call(_, _, _) => {
          if ctx.strict {
            raise @errors.SyntaxError(
              message="Invalid compound assignment target at line \{loc.line}, col \{loc.col}",
            )
          }
          let _ = self.eval_expr(ctx, inner, env)
          raise @errors.ReferenceError(
            message="Invalid left-hand side in assignment",
          )
        }
        _ =>
          raise @errors.SyntaxError(
            message="Invalid compound assignment target at line \{loc.line}, col \{loc.col}",
          )
      }
    }
    _ =>
      raise @errors.SyntaxError(
        message="Invalid compound assignment target at line \{loc.line}, col \{loc.col}",
      )
  }
}

///|
/// Shared skeleton for `&&=`, `||=`, and `??=`.
/// `should_assign(current)` returns true when the operator requires a write.
fn Interpreter::eval_logical_assign(
  self : Interpreter,
  ctx : ExecContext,
  target : @ast.Expr,
  value_expr : @ast.Expr,
  env : Environment,
  loc : @token.Loc,
  should_assign : (Value) -> Bool,
) -> Value raise Error {
  match target {
    Ident(name, _) => {
      let current = get_assignment_name_value(ctx, env, name)
      if should_assign(current) {
        let rhs = self.eval_named_expr(ctx, value_expr, env, name)
        if self.is_immutable_global(name) {
          if ctx.strict {
            raise @errors.TypeError(
              message="Cannot assign to read only property '\{name}' of object '[object global]'",
            )
          }
          return rhs
        }
        env.assign_with_strict(name, rhs, ctx.strict)
        rhs
      } else {
        current
      }
    }
    Member(obj_expr, prop, mloc) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let current = self.get_property(obj, prop, mloc)
      if should_assign(current) {
        let rhs = self.eval_expr(ctx, value_expr, env)
        self.set_property(obj, prop, rhs, loc, strict=ctx.strict)
      } else {
        current
      }
    }
    ComputedMember(obj_expr, key_expr, mloc) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let key = self.eval_expr(ctx, key_expr, env)
      // Per ES5 11.2.1 step 5: CheckObjectCoercible(baseValue) before ToString
      if obj is Null {
        raise @errors.TypeError(message="Cannot read properties of null")
      }
      if obj is Undefined {
        raise @errors.TypeError(message="Cannot read properties of undefined")
      }
      let prop_key = to_property_key(key, interp=Some(self))
      let current = self.get_computed_property(obj, prop_key, mloc)
      if should_assign(current) {
        let rhs = self.eval_expr(ctx, value_expr, env)
        self.set_computed_property(obj, prop_key, rhs, loc, strict=ctx.strict)
      } else {
        current
      }
    }
    PrivateMember(obj_expr, name, _) => {
      let obj = self.eval_expr(ctx, obj_expr, env)
      let current = get_private_member(obj, name, env)
      if should_assign(current) {
        let rhs = self.eval_expr(ctx, value_expr, env)
        let brand = resolve_private_brand(env, name)
        set_private_field(obj, brand, name, rhs)
        rhs
      } else {
        current
      }
    }
    _ =>
      raise @errors.SyntaxError(
        message="Invalid assignment target at line \{loc.line}, col \{loc.col}",
      )
  }
}