//! Interactive "instructions" demo binary.
//!
//! The Rust counterpart of [tests/integration/python/test_instructions.py]: it walks
//! through the exact same instruction scenarios (position, rotation, quaternion, scale,
//! visibility, render order, depth test, transparency, parenting, colors and point clouds)
//! that the Python instructions test documents, pausing for the caller to press Enter
//! between the logical points so the browser-rendered result can be captured.
//!
//! Because the browser renders the *server* state (independent of which client produced
//! it), the release test drives this binary through the same steps as the Python test and
//! compares against the *same* baseline screenshots under `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.

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

use voraus_3d_visu::material::{MeshStandardMaterialOptions, PointsMaterialOptions};
use voraus_3d_visu::obj::boxes::BoxOptions;
use voraus_3d_visu::obj::model::{Model, ModelOptions};
use voraus_3d_visu::obj::point_cloud::PointCloudOptions;
use voraus_3d_visu::obj::types::Transform;
use voraus_3d_visu::schemas::PointCloudFormat;
use voraus_3d_visu::types::Instruction;
use voraus_3d_visu::{ModelSource, Result, Visu};

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

/// The joint pose applied to the robots after the visibility, depth-test and
/// transparency scenarios so the articulated model matches the Python baselines.
const JOINT_POSITIONS: [f64; 6] = [0.0, -1.57, 1.57, -1.57, -1.57, 0.0];

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

/// Connects to the server and plays every instruction scenario interactively.
///
/// # Errors
///
/// Returns any error raised by the underlying client while talking to the server or any
/// I/O error while reading the point cloud fixtures from disk.
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());

    // Resolve the bundled model and point cloud assets relative to this crate so the packed
    // example is self-contained.
    let assets_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let box_glb = assets_dir.join("box.glb");
    let robot_glb = assets_dir.join("robot.glb");
    let objects_dir = assets_dir.join("objects");

    instruction_position(&url, &box_glb)?;
    instruction_rotation(&url, &box_glb)?;
    instruction_quaternion(&url, &box_glb)?;
    instruction_scale(&url, &box_glb)?;
    instruction_visible(&url, &robot_glb)?;
    instruction_render_order(&url, &box_glb)?;
    instruction_depth_test(&url, &robot_glb, &box_glb)?;
    instruction_transparency(&url, &robot_glb, &box_glb)?;
    instruction_parent(&url, &box_glb)?;
    instruction_child(&url, &box_glb)?;
    instruction_color(&url)?;
    instruction_point_cloud(&url, &objects_dir)?;

    Ok(())
}

/// Moves four boxes along the individual axes.
fn instruction_position(url: &str, box_glb: &Path) -> Result<()> {
    // Start-position-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box1 = add_model(&visu, box_glb, [0.0, 0.0, 0.1])?;
    let box2 = add_model(&visu, box_glb, [0.0, 0.0, 0.1])?;
    let box3 = add_model(&visu, box_glb, [0.0, 0.0, 0.1])?;
    let box4 = add_model(&visu, box_glb, [0.0, 0.0, 0.1])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        box1.position().x(1.0),
        box2.position().y(1.0),
        box3.position().z(0.3),
        box4.position().xyz(1.0, 1.0, 0.3),
    ])?;
    // Stop-position-code
    wait_for_enter();
    Ok(())
}

/// Rotates four boxes around the individual axes.
fn instruction_rotation(url: &str, box_glb: &Path) -> Result<()> {
    // Start-rotation-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box1 = add_model(&visu, box_glb, [0.0, -0.75, 0.0])?;
    let box2 = add_model(&visu, box_glb, [0.0, -0.25, 0.0])?;
    let box3 = add_model(&visu, box_glb, [0.0, 0.25, 0.0])?;
    let box4 = add_model(&visu, box_glb, [0.0, 0.75, 0.0])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        box1.rotation().x(1.57),
        box2.rotation().y(1.57),
        box3.rotation().z(1.57),
        box4.rotation().xyz(1.0, 1.0, 1.0),
    ])?;
    // Stop-rotation-code
    wait_for_enter();
    Ok(())
}

/// Rotates a box using a quaternion.
fn instruction_quaternion(url: &str, box_glb: &Path) -> Result<()> {
    // Start-quaternion-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box_model = add_model(&visu, box_glb, [0.0, 0.0, 0.0])?;

    let mut connection = visu.connection()?;
    connection.update(vec![box_model
        .quaternion()
        .xyzw(0.653012, 0.270728, 0.6532274, 0.2712483)])?;
    // Stop-quaternion-code
    wait_for_enter();
    Ok(())
}

/// Scales four boxes along the individual axes.
fn instruction_scale(url: &str, box_glb: &Path) -> Result<()> {
    // Start-scale-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box1 = add_model(&visu, box_glb, [0.0, -0.75, 0.0])?;
    let box2 = add_model(&visu, box_glb, [0.0, -0.25, 0.0])?;
    let box3 = add_model(&visu, box_glb, [0.0, 0.25, 0.0])?;
    let box4 = add_model(&visu, box_glb, [0.0, 0.75, 0.0])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        box1.scale().x(2.0),
        box2.scale().y(2.0),
        box3.scale().z(2.0),
        box4.scale().xyz(2.0, 2.0, 2.0),
    ])?;
    // Stop-scale-code
    wait_for_enter();
    Ok(())
}

/// Hides individual robot parts and a whole robot.
fn instruction_visible(url: &str, robot_glb: &Path) -> Result<()> {
    // Start-visible-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let robot1 = add_model(&visu, robot_glb, [0.0, 0.0, 0.0])?;
    let robot2 = add_model(&visu, robot_glb, [0.0, 1.0, 0.0])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        robot1.child("Base").visible(false),
        robot1.child("Segment2").visible(false),
        robot1.child("Segment4").visible(false),
        robot1.child("Segment6").visible(false),
        robot2.visible(false),
    ])?;
    // Stop-visible-code
    let mut poses = joint_pose(&robot1, JOINT_POSITIONS);
    poses.extend(joint_pose(&robot2, JOINT_POSITIONS));
    connection.update(poses)?;
    wait_for_enter();
    Ok(())
}

/// Overrides the draw order of two coplanar, overlapping transparent boxes so
/// the second one's tint dominates the blend, despite depth alone being
/// ambiguous between them.
fn instruction_render_order(url: &str, box_glb: &Path) -> Result<()> {
    // Start-render-order-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box1 = add_model_unique(&visu, box_glb, [0.0, 0.0, 0.1], true)?;
    let box2 = add_model_unique(&visu, box_glb, [0.2, 0.0, 0.1], true)?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        box1.material()?.color().rgb(1.0, 0.2, 0.2), // orange
        box1.material()?.transparency(0.6)?,
        box1.material()?.depth_test(false),
        box2.material()?.color().rgb(0.1, 0.4, 1.0), // blue
        box2.material()?.transparency(0.6)?,
        box2.material()?.depth_test(false),
        box2.render_order(0.0),
        box1.render_order(1.0),
    ])?;
    // Stop-render-order-code
    wait_for_enter();
    Ok(())
}

/// Disables the depth test of a box so it renders in front of the robot.
fn instruction_depth_test(url: &str, robot_glb: &Path, box_glb: &Path) -> Result<()> {
    // Start-depth-test-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let robot = add_model(&visu, robot_glb, [0.0, 0.0, 0.0])?;
    let box_model = add_model_unique(&visu, box_glb, [-0.5, -0.5, 0.0], true)?;

    let mut connection = visu.connection()?;
    connection.update(vec![box_model.material()?.depth_test(false)])?;
    // Stop-depth-test-code
    connection.update(joint_pose(&robot, JOINT_POSITIONS))?;
    wait_for_enter();
    Ok(())
}

/// Applies different transparency values to robot parts, a robot and a box.
fn instruction_transparency(url: &str, robot_glb: &Path, box_glb: &Path) -> Result<()> {
    // Start-transparency-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let robot1 = add_model_unique(&visu, robot_glb, [0.0, 0.0, 0.0], true)?;
    let robot2 = add_model_unique(&visu, robot_glb, [0.0, 1.0, 0.0], true)?;
    let box1 = add_model_unique(&visu, box_glb, [1.0, 0.0, 0.0], false)?;
    add_model(&visu, box_glb, [1.0, 1.0, 0.0])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        robot1.child("Base").material()?.transparency(0.1)?,
        robot1.child("Segment1").material()?.transparency(1.0)?,
        robot1.child("Segment2").material()?.transparency(0.1)?,
        robot1.child("Segment3").material()?.transparency(1.0)?,
        robot1.child("Segment4").material()?.transparency(0.1)?,
        robot1.child("Segment5").material()?.transparency(0.8)?,
        robot1.child("Segment6").material()?.transparency(0.1)?,
        robot2.material()?.transparency(0.4)?,
        box1.material()?.transparency(0.5)?,
    ])?;
    // Stop-transparency-code
    let mut poses = joint_pose(&robot1, JOINT_POSITIONS);
    poses.extend(joint_pose(&robot2, JOINT_POSITIONS));
    connection.update(poses)?;
    wait_for_enter();
    Ok(())
}

/// Parents one box to another and moves the parent.
fn instruction_parent(url: &str, box_glb: &Path) -> Result<()> {
    // Start-parent-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box1 = add_model(&visu, box_glb, [0.0, 0.0, 0.1])?;
    let box2 = add_model(&visu, box_glb, [0.0, 1.0, 0.1])?;

    let mut connection = visu.connection()?;
    connection.update(vec![box2.parent(Some(&box1)), box1.position().x(2.0)])?;
    // Stop-parent-code
    wait_for_enter();
    Ok(())
}

/// Addresses a child object through its parent.
fn instruction_child(url: &str, box_glb: &Path) -> Result<()> {
    // Start-child-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let box1 = add_model(&visu, box_glb, [0.0, 0.0, 0.1])?;
    let box2 = add_model(&visu, box_glb, [0.0, 1.0, 0.1])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        box2.parent(Some(&box1)),
        box1.position().x(2.0),
        box1.child(&box2.identifier).position().x(-1.0),
    ])?;
    // Stop-child-code
    wait_for_enter();
    Ok(())
}

/// Changes the color of four boxes through their materials.
fn instruction_color(url: &str) -> Result<()> {
    // Start-color-code
    let mut visu = Visu::new(url, false, false, true, None)?;
    let material1 =
        visu.add_mesh_standard_material([0.0, 0.0, 0.0], MeshStandardMaterialOptions::default())?;
    let material2 =
        visu.add_mesh_standard_material([0.0, 0.0, 0.0], MeshStandardMaterialOptions::default())?;
    let material3 =
        visu.add_mesh_standard_material([0.0, 0.0, 0.0], MeshStandardMaterialOptions::default())?;
    let material4 =
        visu.add_mesh_standard_material([0.0, 0.0, 0.0], MeshStandardMaterialOptions::default())?;

    add_box(&visu, &material1.identifier, [0.0, 0.0, 0.0])?;
    add_box(&visu, &material2.identifier, [-2.0, 0.0, 0.0])?;
    add_box(&visu, &material3.identifier, [0.0, -2.0, 0.0])?;
    add_box(&visu, &material4.identifier, [-2.0, -2.0, 0.0])?;

    let mut connection = visu.connection()?;
    connection.update(vec![
        material1.color().r(0.5),
        material2.color().g(0.5),
        material3.color().b(0.5),
        material4.color().rgb(0.5, 0.5, 0.5),
    ])?;
    // Stop-color-code
    wait_for_enter();
    Ok(())
}

/// Uploads point clouds in the PLY (binary and ASCII) and PCD formats.
fn instruction_point_cloud(url: &str, objects_dir: &Path) -> Result<()> {
    let point_cloud_0_ply = objects_dir.join("voraus_0.ply");
    let point_cloud_1_ply = objects_dir.join("voraus_1.ply");
    let point_cloud_0_ply_ascii = objects_dir.join("voraus_0_ascii.ply");
    let point_cloud_1_ply_ascii = objects_dir.join("voraus_1_ascii.ply");
    let point_cloud_0_pcd = objects_dir.join("voraus_0.pcd");
    let point_cloud_1_pcd = objects_dir.join("voraus_1.pcd");

    // Start-points-code
    let mut visu = Visu::new(url, false, false, true, None)?;

    let points_material = visu.add_points_material(
        [0.882, 0.2, 1.0],
        PointsMaterialOptions {
            size: 0.1,
            ..Default::default()
        },
    )?;
    let point_cloud = visu.add_point_cloud(
        PointCloudFormat::Ply,
        PointCloudOptions {
            material: Some(points_material.identifier.clone()),
            ..Default::default()
        },
    )?;

    let mut connection = visu.connection()?;
    let byte_data = std::fs::read(&point_cloud_0_ply)?;
    connection.update(vec![point_cloud.data(&byte_data)])?;
    // Stop-points-code
    wait_for_enter(); // instructions/point_cloud_0.png

    // PLY binary
    connection.delete(&point_cloud)?;
    let point_cloud = connection.add_point_cloud(
        PointCloudFormat::Ply,
        PointCloudOptions {
            data: Some(std::fs::read(&point_cloud_0_ply)?),
            material: Some(points_material.identifier.clone()),
            ..Default::default()
        },
    )?;
    wait_for_enter(); // instructions/point_cloud_0.png

    let ply_1 = std::fs::read(&point_cloud_1_ply)?;
    connection.update(vec![point_cloud.data(&ply_1)])?;
    wait_for_enter(); // instructions/point_cloud_1.png

    // PLY ASCII
    connection.delete(&point_cloud)?;
    let point_cloud = connection.add_point_cloud(
        PointCloudFormat::Ply,
        PointCloudOptions {
            data: Some(std::fs::read(&point_cloud_0_ply_ascii)?),
            material: Some(points_material.identifier.clone()),
            ..Default::default()
        },
    )?;
    wait_for_enter(); // instructions/point_cloud_0.png

    let ply_ascii_1 = std::fs::read(&point_cloud_1_ply_ascii)?;
    connection.update(vec![point_cloud.data(&ply_ascii_1)])?;
    wait_for_enter(); // instructions/point_cloud_1.png

    // PCD
    connection.delete(&point_cloud)?;
    let point_cloud = connection.add_point_cloud(
        PointCloudFormat::Pcd,
        PointCloudOptions {
            data: Some(std::fs::read(&point_cloud_0_pcd)?),
            material: Some(points_material.identifier.clone()),
            ..Default::default()
        },
    )?;
    wait_for_enter(); // instructions/point_cloud_0.png

    let pcd_1 = std::fs::read(&point_cloud_1_pcd)?;
    connection.update(vec![point_cloud.data(&pcd_1)])?;
    wait_for_enter(); // instructions/point_cloud_1.png
    Ok(())
}

/// Adds a box model at `position` and returns its write-access helper.
fn add_model(visu: &Visu, model: &Path, position: [f64; 3]) -> Result<Model> {
    visu.add_model(
        ModelSource::Path(model.to_path_buf()),
        ModelOptions {
            transform: Transform {
                position: Some(position),
                ..Default::default()
            },
            ..Default::default()
        },
    )
}

/// Adds a model at `position` with an explicit `unique_material` flag.
fn add_model_unique(
    visu: &Visu,
    model: &Path,
    position: [f64; 3],
    unique_material: bool,
) -> Result<Model> {
    visu.add_model(
        ModelSource::Path(model.to_path_buf()),
        ModelOptions {
            transform: Transform {
                position: Some(position),
                ..Default::default()
            },
            unique_material: Some(unique_material),
            ..Default::default()
        },
    )
}

/// Adds a unit box at `position` using the material identified by `material`.
fn add_box(visu: &Visu, material: &str, position: [f64; 3]) -> Result<()> {
    visu.add_box(BoxOptions {
        transform: Transform {
            position: Some(position),
            ..Default::default()
        },
        material: Some(material.to_string()),
        ..Default::default()
    })?;
    Ok(())
}

/// Builds the instructions that pose the `CS0`..`CS5` joints of a robot.
fn joint_pose(robot: &Model, joints: [f64; 6]) -> Vec<Instruction> {
    joints
        .iter()
        .enumerate()
        .map(|(index, &angle)| robot.child(&format!("CS{index}")).rotation().z(angle))
        .collect()
}

/// Blocks until the caller advances the walkthrough by sending a newline on stdin.
fn wait_for_enter() {
    print!("Press enter to continue.");
    let _ = io::stdout().flush();
    let mut line = String::new();
    let _ = io::stdin().read_line(&mut line);
}
