//! # orthos
//!
//! High-performance linear algebra library written in Rust.
//!
//! ## Features
//!
//! - Matrix operations: add, subtract, multiply, transpose, inverse, determinant
//! - Vector operations: dot product, norm, normalize
//! - Optional Python bindings via PyO3 (enable with `python` feature)
//!
//! ## Example
//!
//! ```rust
//! use orthos::{Matrix, Vector};
//!
//! // Create a matrix
//! let m = Matrix::from_rows(vec![
//!     vec![1.0, 2.0],
//!     vec![3.0, 4.0],
//! ]).unwrap();
//!
//! // Matrix operations
//! let det = m.determinant().unwrap();
//! let transposed = m.transpose();
//!
//! // Create a vector
//! let v = Vector::new(vec![1.0, 2.0, 3.0]);
//! let norm = v.norm();
//! ```

pub mod error;
pub mod matrix;
pub mod ops;
pub mod vector;

// Re-export core types for Rust users
pub use error::{MatrixError, VectorError};
pub use matrix::Matrix;
pub use vector::Vector;

// Python bindings (only compiled when `python` feature is enabled)
#[cfg(feature = "python")]
mod python_bindings {
    use numpy::{
        PyArray1, PyArray2, PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods, ToPyArray,
    };
    use pyo3::prelude::*;

    use crate::matrix::Matrix;
    use crate::ops;
    use crate::vector::Vector;

    #[pyclass(name = "Matrix")]
    #[derive(Clone)]
    pub struct PyMatrix {
        pub(crate) inner: Matrix,
    }

    #[pymethods]
    impl PyMatrix {
        #[new]
        fn new(data: Vec<Vec<f64>>) -> PyResult<Self> {
            let inner = Matrix::from_rows(data)?;
            Ok(Self { inner })
        }

        #[staticmethod]
        fn zeros(rows: usize, cols: usize) -> Self {
            Self {
                inner: Matrix::zeros(rows, cols),
            }
        }

        #[staticmethod]
        fn ones(rows: usize, cols: usize) -> Self {
            Self {
                inner: Matrix::ones(rows, cols),
            }
        }

        #[staticmethod]
        fn identity(size: usize) -> Self {
            Self {
                inner: Matrix::identity(size),
            }
        }

        #[staticmethod]
        fn from_numpy(array: PyReadonlyArray2<f64>) -> PyResult<Self> {
            let shape = array.shape();
            let rows = shape[0];
            let cols = shape[1];
            let data: Vec<f64> = array.as_array().iter().copied().collect();
            let inner = Matrix::new(rows, cols, data)?;
            Ok(Self { inner })
        }

        #[getter]
        fn shape(&self) -> (usize, usize) {
            self.inner.shape()
        }

        #[getter]
        fn rows(&self) -> usize {
            self.inner.rows()
        }

        #[getter]
        fn cols(&self) -> usize {
            self.inner.cols()
        }

        fn __repr__(&self) -> String {
            format!(
                "Matrix(shape=({}, {}))",
                self.inner.rows(),
                self.inner.cols()
            )
        }

        fn __str__(&self) -> String {
            let data = self.inner.to_vec();
            let rows: Vec<String> = data
                .iter()
                .map(|row| {
                    let elements: Vec<String> = row.iter().map(|x| format!("{:.6}", x)).collect();
                    format!("[{}]", elements.join(", "))
                })
                .collect();
            format!("[{}]", rows.join(",\n "))
        }

        fn __getitem__(&self, idx: (usize, usize)) -> PyResult<f64> {
            Ok(self.inner.get(idx.0, idx.1)?)
        }

        fn __setitem__(&mut self, idx: (usize, usize), value: f64) -> PyResult<()> {
            self.inner.set(idx.0, idx.1, value)?;
            Ok(())
        }

        fn __add__(&self, other: &PyMatrix) -> PyResult<PyMatrix> {
            let inner = self.inner.add(&other.inner)?;
            Ok(PyMatrix { inner })
        }

        fn __sub__(&self, other: &PyMatrix) -> PyResult<PyMatrix> {
            let inner = self.inner.sub(&other.inner)?;
            Ok(PyMatrix { inner })
        }

        fn __mul__(&self, other: &PyMatrix) -> PyResult<PyMatrix> {
            let inner = self.inner.mul_elementwise(&other.inner)?;
            Ok(PyMatrix { inner })
        }

        fn __matmul__(&self, other: &PyMatrix) -> PyResult<PyMatrix> {
            let inner = self.inner.matmul(&other.inner)?;
            Ok(PyMatrix { inner })
        }

        fn __rmul__(&self, scalar: f64) -> PyMatrix {
            PyMatrix {
                inner: self.inner.scale(scalar),
            }
        }

        fn scale(&self, scalar: f64) -> PyMatrix {
            PyMatrix {
                inner: self.inner.scale(scalar),
            }
        }

        fn transpose(&self) -> PyMatrix {
            PyMatrix {
                inner: self.inner.transpose(),
            }
        }

        fn inverse(&self) -> PyResult<PyMatrix> {
            let inner = self.inner.inverse()?;
            Ok(PyMatrix { inner })
        }

        fn determinant(&self) -> PyResult<f64> {
            Ok(self.inner.determinant()?)
        }

        fn trace(&self) -> f64 {
            self.inner.trace()
        }

        fn to_list(&self) -> Vec<Vec<f64>> {
            self.inner.to_vec()
        }

        fn to_numpy<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray2<f64>> {
            let data = self.inner.to_vec();
            let flat: Vec<f64> = data.into_iter().flatten().collect();
            let array = numpy::ndarray::Array2::from_shape_vec(
                (self.inner.rows(), self.inner.cols()),
                flat,
            )
            .unwrap();
            array.to_pyarray(py)
        }

        fn matvec(&self, vector: &PyVector) -> PyResult<PyVector> {
            let inner = ops::matvec(&self.inner, &vector.inner)?;
            Ok(PyVector { inner })
        }
    }

    #[pyclass(name = "Vector")]
    #[derive(Clone)]
    pub struct PyVector {
        pub(crate) inner: Vector,
    }

    #[pymethods]
    impl PyVector {
        #[new]
        fn new(data: Vec<f64>) -> Self {
            Self {
                inner: Vector::new(data),
            }
        }

        #[staticmethod]
        fn zeros(size: usize) -> Self {
            Self {
                inner: Vector::zeros(size),
            }
        }

        #[staticmethod]
        fn ones(size: usize) -> Self {
            Self {
                inner: Vector::ones(size),
            }
        }

        #[staticmethod]
        fn from_numpy(array: PyReadonlyArray1<f64>) -> Self {
            let data: Vec<f64> = array.as_array().iter().copied().collect();
            Self {
                inner: Vector::new(data),
            }
        }

        fn __len__(&self) -> usize {
            self.inner.len()
        }

        fn __repr__(&self) -> String {
            format!("Vector(len={})", self.inner.len())
        }

        fn __str__(&self) -> String {
            let elements: Vec<String> = self
                .inner
                .to_vec()
                .iter()
                .map(|x| format!("{:.6}", x))
                .collect();
            format!("[{}]", elements.join(", "))
        }

        fn __getitem__(&self, idx: usize) -> PyResult<f64> {
            Ok(self.inner.get(idx)?)
        }

        fn __setitem__(&mut self, idx: usize, value: f64) -> PyResult<()> {
            self.inner.set(idx, value)?;
            Ok(())
        }

        fn __add__(&self, other: &PyVector) -> PyResult<PyVector> {
            let inner = self.inner.add(&other.inner)?;
            Ok(PyVector { inner })
        }

        fn __sub__(&self, other: &PyVector) -> PyResult<PyVector> {
            let inner = self.inner.sub(&other.inner)?;
            Ok(PyVector { inner })
        }

        fn __rmul__(&self, scalar: f64) -> PyVector {
            PyVector {
                inner: self.inner.scale(scalar),
            }
        }

        fn scale(&self, scalar: f64) -> PyVector {
            PyVector {
                inner: self.inner.scale(scalar),
            }
        }

        fn dot(&self, other: &PyVector) -> PyResult<f64> {
            Ok(self.inner.dot(&other.inner)?)
        }

        fn norm(&self) -> f64 {
            self.inner.norm()
        }

        fn norm_squared(&self) -> f64 {
            self.inner.norm_squared()
        }

        fn normalize(&self) -> PyResult<PyVector> {
            let inner = self.inner.normalize()?;
            Ok(PyVector { inner })
        }

        fn to_list(&self) -> Vec<f64> {
            self.inner.to_vec()
        }

        fn to_numpy<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray1<f64>> {
            self.inner.to_vec().to_pyarray(py)
        }
    }

    #[pyfunction]
    fn matmul(a: &PyMatrix, b: &PyMatrix) -> PyResult<PyMatrix> {
        let inner = ops::matmul(&a.inner, &b.inner)?;
        Ok(PyMatrix { inner })
    }

    #[pyfunction]
    fn matvec(matrix: &PyMatrix, vector: &PyVector) -> PyResult<PyVector> {
        let inner = ops::matvec(&matrix.inner, &vector.inner)?;
        Ok(PyVector { inner })
    }

    #[pymodule]
    pub fn _orthos(m: &Bound<'_, PyModule>) -> PyResult<()> {
        m.add_class::<PyMatrix>()?;
        m.add_class::<PyVector>()?;
        m.add_function(wrap_pyfunction!(matmul, m)?)?;
        m.add_function(wrap_pyfunction!(matvec, m)?)?;
        Ok(())
    }
}

#[cfg(feature = "python")]
pub use python_bindings::_orthos;
