//! Interactive "box" demo binary.
//!
//! The Rust counterpart of `examples/python/box/box.py`: it connects to a running
//! voraus 3D visualization server and walks through the box scenario, pausing
//! for the user to press Enter between steps so the result can be watched in
//! the browser. It builds a box model that is moved, rotated and toggled, an
//! axes helper and a text panel parented to it, a second box, and two hash
//! (anchor) instructions.
//!
//! The server URL is taken from the first command-line argument, then the
//! `V3DVISU_URL` environment variable, and finally falls back to the
//! `voraus-3d-visu` hostname used by `docker-compose.yml`.

use std::io::{self, Write};
use std::path::PathBuf;
use std::process::ExitCode;

use voraus_3d_visu::obj::axes::AxesOptions;
use voraus_3d_visu::obj::model::ModelOptions;
use voraus_3d_visu::obj::text_panel::TextPanelOptions;
use voraus_3d_visu::obj::types::Transform;
use voraus_3d_visu::{ModelSource, Result, Visu};

/// The default server URL, matching `docker-compose.yml`.
const DEFAULT_URL: &str = "http://voraus-3d-visu/";

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("error: {error}");
            ExitCode::FAILURE
        }
    }
}

/// Connects to the server and plays the box walkthrough interactively.
///
/// # Errors
///
/// Returns any error raised by the underlying client while talking to the
/// server: HTTP requests for object creation and websocket messages for the
/// live updates.
fn run() -> Result<()> {
    let url = std::env::args()
        .nth(1)
        .or_else(|| std::env::var("V3DVISU_URL").ok())
        .unwrap_or_else(|| DEFAULT_URL.to_string());
    let model = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("box.glb");

    // `ModelSource` owns its path, so build a fresh source for every upload. The
    // client hashes the file content and skips the upload when it already
    // exists on the server, so referencing the same model twice is cheap.
    let model_source = || ModelSource::Path(model.clone());

    // `clear_all = true` wipes the whole scene on connect, just like box.py. The meta
    // check is left enabled (as in box.py) so the client verifies it matches the server.
    let mut visu = Visu::new(&url, false, false, true, None)?;
    let mut connection = visu.connection()?;

    let box_model = connection.add_model(
        model_source(),
        ModelOptions {
            transform: Transform {
                position: Some([0.0, 0.0, 0.0]),
                ..Default::default()
            },
            ..Default::default()
        },
    )?;

    wait_for_enter("move and rotate the box.");
    connection.update(vec![
        box_model.position().x(0.3),
        box_model.rotation().z(1.57),
    ])?;

    wait_for_enter("make the box invisible.");
    connection.update(vec![box_model.visible(false)])?;

    wait_for_enter("make the box visible.");
    connection.update(vec![box_model.visible(true)])?;

    wait_for_enter("add axes helpers and parent them to the box.");
    let axes = connection.add_axes(AxesOptions {
        transform: Transform {
            position: Some([0.3, 0.0, 0.0]),
            rotation: Some([0.0, 0.0, 1.57]),
            scale: Some([0.5, 0.5, 0.5]),
            ..Default::default()
        },
        ..Default::default()
    })?;
    connection.update(vec![axes.parent(Some(&box_model))])?;

    wait_for_enter("add another invisible box.");
    let second_box = connection.add_model(
        model_source(),
        ModelOptions {
            transform: Transform {
                position: Some([1.0, 0.0, 0.0]),
                visible: false,
                ..Default::default()
            },
            ..Default::default()
        },
    )?;

    wait_for_enter("make the second box visible.");
    connection.update(vec![second_box.visible(true)])?;

    wait_for_enter("add a text panel.");
    let text_panel = connection.add_text_panel(
        "Position [-1, 0, 0]",
        TextPanelOptions {
            transform: Transform {
                position: Some([-1.0, 0.0, 0.0]),
                ..Default::default()
            },
            ..Default::default()
        },
    )?;

    wait_for_enter("reference the text panel to the box.");
    connection.update(vec![
        text_panel.parent(Some(&box_model)),
        text_panel.position().xyz(0.0, 0.0, 0.0),
        text_panel.text("Box"),
    ])?;

    wait_for_enter("hide the text panel.");
    connection.update(vec![text_panel.visible(false)])?;

    // Register hash (anchor) instructions the frontend can trigger via the URL
    // fragment, e.g. `#invisible` / `#visible`.
    connection.add_hash_instruction(
        "invisible",
        vec![
            box_model.visible(false),
            box_model.position().x(-5.0),
            box_model.scale().z(0.2),
        ],
    )?;
    connection.add_hash_instruction("visible", vec![box_model.visible(true)])?;

    wait_for_enter("exit the program.");
    Ok(())
}

/// Prints the next step and blocks until the user presses Enter.
fn wait_for_enter(prompt: &str) {
    print!("Press enter to {prompt}");
    let _ = io::stdout().flush();
    let mut line = String::new();
    let _ = io::stdin().read_line(&mut line);
}
