use arrow::array::{
    Array, BooleanArray, Date32Array, Float32Array, Float64Array, Int8Array, Int16Array,
    Int32Array, Int64Array, LargeStringArray, StringArray, UInt8Array, UInt16Array, UInt32Array,
    UInt64Array,
};
use arrow::datatypes::DataType;
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::{PyDate, PyList};

use super::{binary, timestamps};

macro_rules! append_array_values {
    ($py:expr, $out:expr, $array:expr, $arr_ty:ty) => {{
        let a = $array
            .as_any()
            .downcast_ref::<$arr_ty>()
            .ok_or_else(|| PyTypeError::new_err(concat!("expected ", stringify!($arr_ty))))?;
        for i in 0..a.len() {
            if a.is_null(i) {
                $out.append($py.None())?;
            } else {
                $out.append(a.value(i))?;
            }
        }
    }};
}

pub(crate) fn array_to_py_list<'py>(
    py: Python<'py>,
    array: &dyn Array,
) -> PyResult<Bound<'py, PyList>> {
    let out = PyList::empty(py);
    match array.data_type() {
        DataType::Int8 => append_array_values!(py, out, array, Int8Array),
        DataType::Int16 => append_array_values!(py, out, array, Int16Array),
        DataType::Int32 => append_array_values!(py, out, array, Int32Array),
        DataType::Int64 => append_array_values!(py, out, array, Int64Array),
        DataType::UInt8 => append_array_values!(py, out, array, UInt8Array),
        DataType::UInt16 => append_array_values!(py, out, array, UInt16Array),
        DataType::UInt32 => append_array_values!(py, out, array, UInt32Array),
        DataType::UInt64 => append_array_values!(py, out, array, UInt64Array),
        DataType::Float32 => append_array_values!(py, out, array, Float32Array),
        DataType::Float64 => append_array_values!(py, out, array, Float64Array),
        DataType::Boolean => append_array_values!(py, out, array, BooleanArray),
        DataType::Utf8 => append_array_values!(py, out, array, StringArray),
        DataType::LargeUtf8 => append_array_values!(py, out, array, LargeStringArray),
        DataType::Binary => binary::append_binary_as_utf8(py, &out, array, false)?,
        DataType::LargeBinary => binary::append_binary_as_utf8(py, &out, array, true)?,
        DataType::Date32 => append_date32_values(py, &out, array)?,
        DataType::Timestamp(unit, tz) => {
            timestamps::append_timestamp_values(py, &out, array, unit, tz.is_some())?
        }
        _ => {
            return Err(PyTypeError::new_err(format!(
                "unsupported Arrow data type for Python conversion: {}",
                array.data_type()
            )));
        }
    }
    Ok(out)
}

fn append_date32_values<'py>(
    py: Python<'py>,
    out: &Bound<'py, PyList>,
    array: &dyn Array,
) -> PyResult<()> {
    let a = array
        .as_any()
        .downcast_ref::<Date32Array>()
        .ok_or_else(|| PyTypeError::new_err("expected Date32Array"))?;
    for i in 0..a.len() {
        if a.is_null(i) {
            out.append(py.None())?;
        } else {
            let date = a
                .value_as_date(i)
                .ok_or_else(|| PyTypeError::new_err("invalid Date32 value"))?;
            let iso = date.to_string();
            let mut parts = iso.split('-');
            let year = parts
                .next()
                .ok_or_else(|| PyTypeError::new_err("invalid Date32 year"))?
                .parse::<i32>()
                .map_err(|_| PyTypeError::new_err("invalid Date32 year"))?;
            let month = parts
                .next()
                .ok_or_else(|| PyTypeError::new_err("invalid Date32 month"))?
                .parse::<u8>()
                .map_err(|_| PyTypeError::new_err("invalid Date32 month"))?;
            let day = parts
                .next()
                .ok_or_else(|| PyTypeError::new_err("invalid Date32 day"))?
                .parse::<u8>()
                .map_err(|_| PyTypeError::new_err("invalid Date32 day"))?;
            out.append(PyDate::new(py, year, month, day)?)?;
        }
    }
    Ok(())
}
