use crate::bvh::BVH;
use crate::fit::DetectedPrimitive;
use crate::mesh::{AABB, TriangleMesh, Vec3};
use crate::reconstruct::ideal_outward_normal;
use crate::sparse_grid::SurfaceSample;
use cord_riesz::MonogenicField;

/// Classify each primitive as subtractive using the monogenic signal.
///
/// Evaluates the mesh SDF on a regular N³ grid, computes the Riesz
/// transform, and compares monogenic orientation at each primitive's
/// support points against the primitive's ideal outward normal.
///
/// At surface crossings (monogenic phase ≈ π/2), the Riesz orientation
/// vector points from negative SDF (interior) toward positive SDF
/// (exterior). If this opposes a primitive's ideal outward normal,
/// the primitive represents removed material — a subtractive operation.
pub fn classify_subtractive(
    mesh: &TriangleMesh,
    bvh: &BVH,
    bounds: &AABB,
    primitives: &[DetectedPrimitive],
    samples: &[SurfaceSample],
    resolution: usize,
) -> Vec<bool> {
    let n = resolution.max(8);
    let field = evaluate_sdf_grid(mesh, bvh, bounds, n);
    let mono = MonogenicField::compute(&field, n);

    primitives
        .iter()
        .map(|prim| classify_one(prim, samples, &mono, bounds, n))
        .collect()
}

/// Sample the mesh SDF on a regular N³ grid.
fn evaluate_sdf_grid(
    mesh: &TriangleMesh,
    bvh: &BVH,
    bounds: &AABB,
    n: usize,
) -> Vec<f64> {
    let step = [
        (bounds.max.x - bounds.min.x) / (n - 1).max(1) as f64,
        (bounds.max.y - bounds.min.y) / (n - 1).max(1) as f64,
        (bounds.max.z - bounds.min.z) / (n - 1).max(1) as f64,
    ];

    let mut field = Vec::with_capacity(n * n * n);
    for iz in 0..n {
        let z = bounds.min.z + iz as f64 * step[2];
        for iy in 0..n {
            let y = bounds.min.y + iy as f64 * step[1];
            for ix in 0..n {
                let x = bounds.min.x + ix as f64 * step[0];
                field.push(bvh.signed_distance(mesh, Vec3::new(x, y, z)));
            }
        }
    }
    field
}

/// Map a world position to the nearest grid index.
fn world_to_grid_index(pos: Vec3, bounds: &AABB, n: usize) -> usize {
    let extent = bounds.max - bounds.min;
    let nf = (n - 1) as f64;

    let ix = ((pos.x - bounds.min.x) / extent.x * nf)
        .round()
        .clamp(0.0, nf) as usize;
    let iy = ((pos.y - bounds.min.y) / extent.y * nf)
        .round()
        .clamp(0.0, nf) as usize;
    let iz = ((pos.z - bounds.min.z) / extent.z * nf)
        .round()
        .clamp(0.0, nf) as usize;

    iz * n * n + iy * n + ix
}

/// Classify a single primitive via monogenic orientation voting.
///
/// For each support point near a surface crossing (phase ≈ π/2),
/// the Riesz orientation vector is compared against the primitive's
/// ideal outward normal. Majority opposition → subtractive.
///
/// Falls back to raw mesh normal comparison when too few support
/// points land on monogenic edge features.
fn classify_one(
    prim: &DetectedPrimitive,
    samples: &[SurfaceSample],
    mono: &MonogenicField,
    bounds: &AABB,
    n: usize,
) -> bool {
    if prim.support.is_empty() {
        return false;
    }

    let half_pi = std::f64::consts::FRAC_PI_2;
    let phase_tol = 0.8;
    let min_amplitude = 1e-6;

    let mut agree = 0usize;
    let mut oppose = 0usize;

    for &idx in &prim.support {
        if idx >= samples.len() {
            continue;
        }
        let sample = &samples[idx];
        let grid_idx = world_to_grid_index(sample.position, bounds, n);
        if grid_idx >= mono.samples.len() {
            continue;
        }

        let ms = &mono.samples[grid_idx];

        // Only count samples near edge phase with significant amplitude
        if ms.amplitude < min_amplitude || (ms.phase - half_pi).abs() > phase_tol {
            continue;
        }

        let ideal = ideal_outward_normal(&prim.kind, sample.position);
        let dot = ideal.x * ms.orientation[0]
            + ideal.y * ms.orientation[1]
            + ideal.z * ms.orientation[2];

        if dot > 0.0 {
            agree += 1;
        } else {
            oppose += 1;
        }
    }

    // Fallback: if too few monogenic edge samples, use raw mesh normals
    if agree + oppose < 3 {
        return fallback_normal_classify(prim, samples);
    }

    oppose > agree
}

/// Normal-comparison fallback: compare mesh normals against the
/// primitive's ideal outward normal via majority vote.
fn fallback_normal_classify(prim: &DetectedPrimitive, samples: &[SurfaceSample]) -> bool {
    let mut agree = 0usize;
    let mut oppose = 0usize;

    for &idx in &prim.support {
        if idx >= samples.len() {
            continue;
        }
        let sample = &samples[idx];
        let ideal = ideal_outward_normal(&prim.kind, sample.position);
        let dot = ideal.dot(sample.normal);
        if dot > 0.0 {
            agree += 1;
        } else {
            oppose += 1;
        }
    }

    oppose > agree
}
