use core::fmt::Write as _;
use std::io;
use std::path::{Path, PathBuf};

use bone_document::{
    Document, ExtrudeError, FeatureNode, LabelSidecar, evaluate_extrude, evaluate_sketch,
};
use bone_kernel::{BrepError, BrepSolid};
use bone_types::{
    FaceRole, FeatureId, StepFileHeader, StepFileName, StepOrganization, StepOriginatingSystem,
    StepSchema,
};

const PINNED_TIMESTAMP: &str = "1970-01-01T00:00:00";
const ORIGINATING_SYSTEM: &str = concat!("Bone ", env!("CARGO_PKG_VERSION"));

const fn schema_token(schema: StepSchema) -> &'static str {
    match schema {
        StepSchema::Ap214 => "AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }",
        StepSchema::Ap242E2 => {
            "AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF { 1 0 10303 442 3 1 4 }"
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum StepError {
    #[error("schema {0} is unsupported for export; only AP214 is emitted")]
    SchemaUnsupported(StepSchema),
    #[error("phase-2 step export writes a single body per document")]
    BodyCount { count: usize },
    #[error("extrude feature {feature:?} has no evaluable sketch")]
    DanglingExtrude { feature: FeatureId },
    #[error("feature {feature:?} failed to evaluate")]
    Evaluation {
        feature: FeatureId,
        #[source]
        source: ExtrudeError,
    },
    #[error("io at {path}: {source}")]
    Io {
        path: PathBuf,
        #[source]
        source: io::Error,
    },
    #[error("step geometry: {0}")]
    Geometry(#[from] BrepError),
    #[error("label sidecar at {path}: {source}")]
    Sidecar {
        path: PathBuf,
        #[source]
        source: Box<bone_document::RonError>,
    },
}

pub enum ImportOutcome {
    Labeled(BrepSolid),
    Imported(BrepSolid),
}

impl ImportOutcome {
    #[must_use]
    pub fn solid(&self) -> &BrepSolid {
        match self {
            Self::Labeled(solid) | Self::Imported(solid) => solid,
        }
    }

    #[must_use]
    pub fn into_solid(self) -> BrepSolid {
        match self {
            Self::Labeled(solid) | Self::Imported(solid) => solid,
        }
    }

    #[must_use]
    pub fn is_labeled(&self) -> bool {
        matches!(self, Self::Labeled(_))
    }
}

pub fn write(document: &Document, path: &Path, schema: StepSchema) -> Result<(), StepError> {
    if schema != StepSchema::Ap214 {
        return Err(StepError::SchemaUnsupported(schema));
    }
    let solid = body_of(document)?;
    let body = solid.to_step_body()?;
    let contents = envelope(
        &render_header(&export_header(document.name(), schema)),
        &body,
    );
    let labels_path = sidecar_path(path);
    let sidecar = LabelSidecar::capture(&solid)
        .to_ron()
        .map_err(|source| StepError::Sidecar {
            path: labels_path.clone(),
            source: Box::new(source),
        })?;
    write_file(&labels_path, sidecar.as_bytes())?;
    write_file(path, contents.as_bytes())
}

pub fn body_of(document: &Document) -> Result<BrepSolid, StepError> {
    let extrudes: Vec<FeatureId> = document
        .feature_tree()
        .iter()
        .filter_map(|(feature, node)| matches!(node, FeatureNode::Extrude(_)).then_some(feature))
        .collect();
    let [feature] = extrudes.as_slice() else {
        return Err(StepError::BodyCount {
            count: extrudes.len(),
        });
    };
    let feature = *feature;
    let extrude = document
        .extrude_of_feature(feature)
        .ok_or(StepError::DanglingExtrude { feature })?;
    let sketch = document
        .sketch(extrude.sketch)
        .ok_or(StepError::DanglingExtrude { feature })?;
    evaluate_extrude(feature, &evaluate_sketch(sketch), extrude)
        .result()
        .clone()
        .map_err(|source| StepError::Evaluation { feature, source })
}

pub fn read(path: &Path, feature: FeatureId) -> Result<ImportOutcome, StepError> {
    let text = read_file(path)?;
    let sidecar = read_sidecar(path)?;
    let solid = BrepSolid::from_step(
        &text,
        feature,
        sidecar.as_ref().map(|side| (side.solid(), side.reattach())),
    )?;
    let labeled = solid
        .iter_faces()
        .any(|face| !matches!(face.label().role, FaceRole::Imported { .. }));
    Ok(if labeled {
        ImportOutcome::Labeled(solid)
    } else {
        ImportOutcome::Imported(solid)
    })
}

fn envelope(header: &str, body: &str) -> String {
    format!("ISO-10303-21;\n{header}DATA;\n{body}ENDSEC;\nEND-ISO-10303-21;\n")
}

fn export_header(file_name: &str, schema: StepSchema) -> StepFileHeader {
    StepFileHeader {
        schema,
        originating_system: StepOriginatingSystem::new(ORIGINATING_SYSTEM),
        organization: StepOrganization::new(""),
        file_name: StepFileName::new(file_name),
    }
}

fn render_header(header: &StepFileHeader) -> String {
    format!(
        "HEADER;\nFILE_DESCRIPTION(('Bone geometry'),'2;1');\nFILE_NAME('{name}','{PINNED_TIMESTAMP}',(''),('{organization}'),'','{system}','');\nFILE_SCHEMA(('{schema}'));\nENDSEC;\n",
        name = escape_step_string(header.file_name.as_str()),
        organization = escape_step_string(header.organization.as_str()),
        system = escape_step_string(header.originating_system.as_str()),
        schema = schema_token(header.schema),
    )
}

fn escape_step_string(value: &str) -> String {
    value.chars().map(escape_step_char).collect()
}

fn escape_step_char(ch: char) -> String {
    match ch {
        '\'' => "''".to_string(),
        '\\' => "\\\\".to_string(),
        c if c.is_ascii_graphic() || c == ' ' => c.to_string(),
        c => {
            let mut units = [0u16; 2];
            c.encode_utf16(&mut units)
                .iter()
                .fold(String::from("\\X2\\"), |mut acc, unit| {
                    let _ = write!(acc, "{unit:04X}");
                    acc
                })
                + "\\X0\\"
        }
    }
}

fn sidecar_path(step: &Path) -> PathBuf {
    let mut name = step.as_os_str().to_os_string();
    name.push(".labels");
    PathBuf::from(name)
}

fn write_file(path: &Path, bytes: &[u8]) -> Result<(), StepError> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).map_err(|source| StepError::Io {
            path: parent.to_path_buf(),
            source,
        })?;
    }
    std::fs::write(path, bytes).map_err(|source| StepError::Io {
        path: path.to_path_buf(),
        source,
    })
}

fn read_file(path: &Path) -> Result<String, StepError> {
    std::fs::read_to_string(path).map_err(|source| StepError::Io {
        path: path.to_path_buf(),
        source,
    })
}

fn read_sidecar(step: &Path) -> Result<Option<LabelSidecar>, StepError> {
    let path = sidecar_path(step);
    match std::fs::read_to_string(&path) {
        Ok(text) => LabelSidecar::from_ron(&text)
            .map(Some)
            .map_err(|source| StepError::Sidecar {
                path,
                source: Box::new(source),
            }),
        Err(ref source) if source.kind() == io::ErrorKind::NotFound => Ok(None),
        Err(source) => Err(StepError::Io { path, source }),
    }
}
