use bone_solver::{
    ConstraintSystem, DofConfig, NewtonConfig, SolverError, analyze_dof, analyze_dof_at, decompose,
    dedup_preserving_order, minimal_unsatisfiable_subset, solve_newton_decomposed,
};
use std::collections::BTreeSet;

use bone_types::{DegreesOfFreedom, ResidualIndex, SketchEntityId, SketchItemId, SketchStatus};

#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum DragError {
    #[error("dragged entity {0:?} is not a Point")]
    NotAPoint(SketchEntityId),
    #[error("dragged entity {0:?} not found in sketch")]
    NotFound(SketchEntityId),
    #[error(transparent)]
    Solver(#[from] SolverError),
}

pub(crate) fn narrow_conflicts_with(
    system: &ConstraintSystem,
    over_flagged: &[ResidualIndex],
    items_of: impl Fn(&[ResidualIndex]) -> Vec<SketchItemId>,
) -> Vec<SketchItemId> {
    let mus = minimal_unsatisfiable_subset(system, over_flagged, DofConfig::DEFAULT);
    let rows = if mus.is_empty() { over_flagged } else { &mus };
    dedup_preserving_order(items_of(rows))
}

pub(crate) fn status_from_solve(
    system: &ConstraintSystem,
    items_of: impl Fn(&[ResidualIndex]) -> Vec<SketchItemId>,
    redundant_of: impl Fn(&BTreeSet<ResidualIndex>) -> Vec<SketchItemId>,
) -> SketchStatusReport {
    let decomposition = decompose(system);
    match solve_newton_decomposed(system, &decomposition, NewtonConfig::DEFAULT) {
        Ok(parameters) => {
            let report = analyze_dof_at(system, &parameters, DofConfig::DEFAULT);
            let redundant_rows: BTreeSet<ResidualIndex> = report
                .redundant_parents()
                .iter()
                .flat_map(|&parent| system.rows_of_parent(parent))
                .collect();
            let redundant = redundant_of(&redundant_rows);
            if !report.over_constrained().is_empty() {
                return SketchStatusReport::over_defined(narrow_conflicts_with(
                    system,
                    report.over_constrained(),
                    &items_of,
                ))
                .with_redundant(redundant);
            }
            if report.dof().value() > 0 {
                SketchStatusReport::under_defined(report.dof()).with_redundant(redundant)
            } else {
                SketchStatusReport::fully_defined().with_redundant(redundant)
            }
        }
        Err(SolverError::OverDefined { conflicts }) => SketchStatusReport::over_defined(conflicts),
        Err(SolverError::NoSolutionFound { .. }) => {
            let report = analyze_dof(system, DofConfig::DEFAULT);
            if report.over_constrained().is_empty() {
                SketchStatusReport::no_solution_found()
            } else {
                SketchStatusReport::over_defined(narrow_conflicts_with(
                    system,
                    report.over_constrained(),
                    &items_of,
                ))
            }
        }
        Err(SolverError::InvalidSolutionFound { .. }) => {
            SketchStatusReport::invalid_solution_found()
        }
        Err(SolverError::Budget { .. }) => SketchStatusReport::no_solution_found(),
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SketchStatusReport {
    status: SketchStatus,
    offending: Vec<SketchItemId>,
    redundant: Vec<SketchItemId>,
    dof: DegreesOfFreedom,
}

impl SketchStatusReport {
    #[must_use]
    pub fn status(&self) -> SketchStatus {
        self.status
    }

    #[must_use]
    pub fn offending(&self) -> &[SketchItemId] {
        &self.offending
    }

    #[must_use]
    pub fn redundant(&self) -> &[SketchItemId] {
        &self.redundant
    }

    #[must_use]
    pub fn dof(&self) -> DegreesOfFreedom {
        self.dof
    }

    #[must_use]
    pub fn with_offending(self, offending: Vec<SketchItemId>) -> Self {
        Self { offending, ..self }
    }

    #[must_use]
    pub fn with_redundant(self, redundant: Vec<SketchItemId>) -> Self {
        Self { redundant, ..self }
    }

    pub(crate) fn fully_defined() -> Self {
        Self {
            status: SketchStatus::FullyDefined,
            offending: Vec::new(),
            redundant: Vec::new(),
            dof: DegreesOfFreedom::new(0),
        }
    }

    pub(crate) fn under_defined(dof: DegreesOfFreedom) -> Self {
        Self {
            status: SketchStatus::UnderDefined,
            offending: Vec::new(),
            redundant: Vec::new(),
            dof,
        }
    }

    pub(crate) fn over_defined(offending: Vec<SketchItemId>) -> Self {
        Self {
            status: SketchStatus::OverDefined,
            offending,
            redundant: Vec::new(),
            dof: DegreesOfFreedom::new(0),
        }
    }

    pub(crate) fn no_solution_found() -> Self {
        Self {
            status: SketchStatus::NoSolutionFound,
            offending: Vec::new(),
            redundant: Vec::new(),
            dof: DegreesOfFreedom::new(0),
        }
    }

    pub(crate) fn invalid_solution_found() -> Self {
        Self {
            status: SketchStatus::InvalidSolutionFound,
            offending: Vec::new(),
            redundant: Vec::new(),
            dof: DegreesOfFreedom::new(0),
        }
    }

    pub(crate) fn dangling(offending: Vec<SketchItemId>) -> Self {
        Self {
            status: SketchStatus::Dangling,
            offending,
            redundant: Vec::new(),
            dof: DegreesOfFreedom::new(0),
        }
    }
}
