//! Standard library shells.

use anyhow::Result;
use derive_docs::stdlib;
use kittycad::types::ModelingCmd;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{
    errors::{KclError, KclErrorDetails},
    executor::{ExtrudeGroup, KclValue},
    std::{sketch::FaceTag, Args},
};

/// Data for shells.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ShellData {
    /// The thickness of the shell.
    pub thickness: f64,
    /// The faces you want removed.
    pub faces: Vec<FaceTag>,
}

/// Create a shell.
pub async fn shell(args: Args) -> Result<KclValue, KclError> {
    let (data, extrude_group): (ShellData, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;

    let extrude_group = inner_shell(data, extrude_group, args).await?;
    Ok(KclValue::ExtrudeGroup(extrude_group))
}

/// Remove volume from a 3-dimensional shape such that a wall of the
/// provided thickness remains, taking volume starting at the provided
/// face, leaving it open in that direction.
///
/// ```no_run
/// const firstSketch = startSketchOn('XY')
///     |> startProfileAt([-12, 12], %)
///     |> line([24, 0], %)
///     |> line([0, -24], %)
///     |> line([-24, 0], %)
///     |> close(%)
///     |> extrude(6, %)
///
/// // Remove the end face for the extrusion.
/// shell({
///     faces: ['end'],
///     thickness: 0.25,
/// }, firstSketch)
/// ```
///
/// ```no_run
/// const firstSketch = startSketchOn('-XZ')
///     |> startProfileAt([-12, 12], %)
///     |> line([24, 0], %)
///     |> line([0, -24], %)
///     |> line([-24, 0], %)
///     |> close(%)
///     |> extrude(6, %)
///
/// // Remove the start face for the extrusion.
/// shell({
///     faces: ['start'],
///     thickness: 0.25,
/// }, firstSketch)
/// ```
///
/// ```no_run
/// const firstSketch = startSketchOn('XY')
///     |> startProfileAt([-12, 12], %)
///     |> line([24, 0], %)
///     |> line([0, -24], %)
///     |> line([-24, 0], %, $myTag)
///     |> close(%)
///     |> extrude(6, %)
///
/// // Remove a tagged face for the extrusion.
/// shell({
///     faces: [myTag],
///     thickness: 0.25,
/// }, firstSketch)
/// ```
#[stdlib {
    name = "shell",
}]
async fn inner_shell(
    data: ShellData,
    extrude_group: Box<ExtrudeGroup>,
    args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
    if data.faces.is_empty() {
        return Err(KclError::Type(KclErrorDetails {
            message: "Expected at least one face".to_string(),
            source_ranges: vec![args.source_range],
        }));
    }

    let mut face_ids = Vec::new();
    for tag in data.faces {
        let extrude_plane_id = tag.get_face_id(&extrude_group, &args, false).await?;

        face_ids.push(extrude_plane_id);
    }

    if face_ids.is_empty() {
        return Err(KclError::Type(KclErrorDetails {
            message: "Expected at least one valid face".to_string(),
            source_ranges: vec![args.source_range],
        }));
    }

    // Flush the batch for our fillets/chamfers if there are any.
    // If we do not do these for sketch on face, things will fail with face does not exist.
    args.flush_batch_for_extrude_group_set(extrude_group.clone().into())
        .await?;

    args.batch_modeling_cmd(
        uuid::Uuid::new_v4(),
        ModelingCmd::Solid3DShellFace {
            hollow: false,
            face_ids,
            object_id: extrude_group.id,
            shell_thickness: data.thickness,
        },
    )
    .await?;

    Ok(extrude_group)
}

/// Make the inside of a 3D object hollow.
pub async fn hollow(args: Args) -> Result<KclValue, KclError> {
    let (thickness, extrude_group): (f64, Box<ExtrudeGroup>) = args.get_data_and_extrude_group()?;

    let extrude_group = inner_hollow(thickness, extrude_group, args).await?;
    Ok(KclValue::ExtrudeGroup(extrude_group))
}

/// Make the inside of a 3D object hollow.
///
/// Remove volume from a 3-dimensional shape such that a wall of the
/// provided thickness remains around the exterior of the shape.
///
/// ```no_run
/// const firstSketch = startSketchOn('XY')
///     |> startProfileAt([-12, 12], %)
///     |> line([24, 0], %)
///     |> line([0, -24], %)
///     |> line([-24, 0], %)
///     |> close(%)
///     |> extrude(6, %)
///     |> hollow (0.25, %)
/// ```
///
/// ```no_run
/// const firstSketch = startSketchOn('-XZ')
///     |> startProfileAt([-12, 12], %)
///     |> line([24, 0], %)
///     |> line([0, -24], %)
///     |> line([-24, 0], %)
///     |> close(%)
///     |> extrude(6, %)
///     |> hollow (0.5, %)
/// ```
#[stdlib {
    name = "hollow",
}]
async fn inner_hollow(
    thickness: f64,
    extrude_group: Box<ExtrudeGroup>,
    args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> {
    // Flush the batch for our fillets/chamfers if there are any.
    // If we do not do these for sketch on face, things will fail with face does not exist.
    args.flush_batch_for_extrude_group_set(extrude_group.clone().into())
        .await?;

    args.batch_modeling_cmd(
        uuid::Uuid::new_v4(),
        ModelingCmd::Solid3DShellFace {
            hollow: true,
            face_ids: Vec::new(), // This is empty because we want to hollow the entire object.
            object_id: extrude_group.id,
            shell_thickness: thickness,
        },
    )
    .await?;

    Ok(extrude_group)
}
