// Code generated by scripts/generate-mbt-blender-sdk.py — DO NOT EDIT.

///|
/// Client manages a connection to the Blender MCP server.
/// It launches the server as a subprocess and communicates via JSON-RPC over stdio.
/// All tool methods are async and must be called within an `async fn main` block.
///
/// Usage:
///   async fn main {
///     let client = @blender.new_client("")
///     let res = client.execute_blender_code("import bpy; result = {'count': len(bpy.data.objects)}")
///     println(res)
///     client.close()
///   }
///
pub struct Client {
  pid : Int
  stdin : @process.WriteToProcess
  stdout : @process.ReadFromProcess
  mut msg_id : Int
}

///|
/// default_server_bin returns the default command to launch the Blender MCP server.
///
let default_server_bin : String = "uv"

///|
/// VERSION is the semver version of this package.
///
const VERSION : String = "0.2.0"

///|
/// default_server_args returns the default arguments for the Blender MCP server.
///
let default_server_args : Array[String] = [
  "--directory",
  expand_path("~/Projects/Blender/blender_mcp/mcp/blmcp"),
  "run",
  "blender-mcp",
]

///|
/// new_client launches the Blender MCP server and returns a Client.
/// If server_bin is empty, uses the default (uv run blender-mcp).
/// Must be called within an async function.
///
pub async fn new_client(server_bin : String) -> Client {
  let (cmd, args) = if server_bin == "" {
    (default_server_bin, default_server_args)
  } else {
    (server_bin, [])
  }
  let path = expand_path(cmd)
  let (stdin_read, stdin_write) = @process.write_to_process()
  let (stdout_read, stdout_write) = @process.read_from_process()
  let pid = @process.spawn_orphan(
    path,
    args,
    stdin=stdin_read,
    stdout=stdout_write,
  )
  let client = Client::{
    pid,
    stdin: stdin_write,
    stdout: stdout_read,
    msg_id: 0,
  }
  client.initialize()
  client
}

///|
/// close shuts down the MCP server process.
///
pub fn Client::close(self : Client) -> Unit {
  self.stdin.close()
  self.stdout.close()
}

///|
/// expand_path expands a leading ~ to the user's home directory.
///
fn expand_path(path : String) -> String {
  if path.has_prefix("~") {
    let home = @env.get_env_var("HOME").unwrap_or("")
    home + path[1:].to_owned()
  } else {
    path
  }
}

// --- JSON-RPC internals ---

///|
/// next_id returns the next JSON-RPC request ID.
///
fn Client::next_id(self : Client) -> Int {
  self.msg_id = self.msg_id + 1
  self.msg_id
}

///|
/// send_line writes a JSON-RPC message to the server's stdin.
///
async fn Client::send_line(self : Client, json_str : String) -> Unit {
  let data = @utf8.encode(json_str + "\n")
  let _ = self.stdin.write_once(data, offset=0, len=data.length())
}

///|
/// read_line reads a JSON-RPC response line from the server's stdout.
///
async fn Client::read_line(self : Client) -> String {
  match self.stdout.read_until("\n") {
    Some(line) => line
    None => raise Failure::Failure("EOF: no more data from server")
  }
}

///|
/// call_tool invokes an MCP tool by name and returns the text response.
///
pub async fn Client::call_tool(
  self : Client,
  tool_name : String,
  args : Map[String, Json],
) -> String {
  let id = self.next_id()
  let req = Json::object(
    Map([
      ("jsonrpc", Json::string("2.0")),
      ("id", Json::number(id.to_double())),
      ("method", Json::string("tools/call")),
      (
        "params",
        Json::object(
          Map([
            ("name", Json::string(tool_name)),
            ("arguments", Json::object(args)),
          ]),
        ),
      ),
    ]),
  )
  self.send_line(req.stringify())
  let response = self.read_line()
  try @json.parse(response) catch {
    _ => return response
  } noraise {
    resp =>
      match resp {
        Object(fields) => {
          let result = fields.get("result").unwrap()
          match result {
            Object(result_fields) => {
              let content = result_fields.get("content").unwrap()
              match content {
                Array(items) => {
                  let mut text = ""
                  for item in items {
                    match item {
                      Object(item_fields) =>
                        match item_fields.get("text") {
                          Some(String(s)) => text = text + s
                          _ => ()
                        }
                      _ => ()
                    }
                  }
                  text
                }
                _ => raise Failure::Failure("unexpected content format")
              }
            }
            _ => raise Failure::Failure("unexpected result format")
          }
        }
        _ => response
      }
  }
}

///|
/// initialize performs the MCP initialize handshake.
///
async fn Client::initialize(self : Client) -> Unit {
  let id = self.next_id()
  let req = Json::object(
    Map([
      ("jsonrpc", Json::string("2.0")),
      ("id", Json::number(id.to_double())),
      ("method", Json::string("initialize")),
      (
        "params",
        Json::object(
          Map([
            ("protocolVersion", Json::string("2024-11-05")),
            ("capabilities", Json::object(Map([]))),
            (
              "clientInfo",
              Json::object(
                Map([
                  ("name", Json::string("blender")),
                  ("version", Json::string(VERSION)),
                ]),
              ),
            ),
          ]),
        ),
      ),
    ]),
  )
  self.send_line(req.stringify())
  let _ = self.read_line()
  let notif = Json::object(
    Map([
      ("jsonrpc", Json::string("2.0")),
      ("method", Json::string("notifications/initialized")),
    ]),
  )
  self.send_line(notif.stringify())
}