use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use utiles::zoom::ZoomOrZooms;

use crate::pyutiles::pybbox::PyBbox;
use crate::pyutiles::pylnglat::PyLngLat;
use crate::pyutiles::pylnglatbbox::PyLngLatBbox;
use crate::pyutiles::pyparsing::PyTileArg;
use crate::pyutiles::pytile::PyTile;
use crate::pyutiles::pytile_tuple::TileTuple;
use crate::pyutiles::pytilelike::PyTileLike;
use crate::pyutiles::pytiles_generator::TilesGenerator;
use crate::pyutiles::zoom::PyZoomOrZooms;
use crate::pyutiles::{pycoords, pyparsing};

#[pyfunction]
pub(crate) fn xyz(x: u32, y: u32, z: u8) -> PyTile {
    PyTile::py_new(x, y, z)
}

#[pyfunction]
#[pyo3(signature = (*args))]
pub(crate) fn ul(args: PyTileArg) -> PyLngLat {
    args.ul()
}

#[pyfunction]
#[pyo3(signature = (lng, lat, truncate = None))]
pub(crate) fn xy(lng: f64, lat: f64, truncate: Option<bool>) -> (f64, f64) {
    utiles::xy(lng, lat, truncate)
}

#[pyfunction]
#[pyo3(signature = (lng, lat, truncate = None))]
pub(crate) fn _xy(lng: f64, lat: f64, truncate: Option<bool>) -> PyResult<(f64, f64)> {
    let trunc = truncate.unwrap_or(false);
    if !trunc && (lat <= -90.0 || lat >= 90.0) {
        Err(PyErr::new::<PyValueError, _>(format!(
            "Invalid latitude: {lat}"
        )))?;
    }
    let xy = utiles::_xy(lng, lat, truncate);
    match xy {
        Ok(xy) => Ok(xy),
        Err(_e) => Err(PyErr::new::<PyValueError, _>(format!(
            "Invalid latitude: {lat}"
        )))?,
    }
}

#[pyfunction]
#[pyo3(signature = (x, y, truncate = None))]
pub(crate) fn lnglat(x: f64, y: f64, truncate: Option<bool>) -> PyLngLat {
    let lnglat = utiles::lnglat(x, y, truncate);
    PyLngLat::py_new(lnglat.lng(), lnglat.lat())
}

#[pyfunction]
#[pyo3(signature = (*args))]
pub(crate) fn bounds(args: PyTileArg) -> PyLngLatBbox {
    args.bounds()
}

#[pyfunction]
pub(crate) fn minmax(zoom: i32) -> PyResult<(u32, u32)> {
    if !(0..=32).contains(&zoom) {
        Err(PyErr::new::<PyValueError, _>(format!(
            "zoom must be between 0 and 32: {zoom}"
        )))?;
    }
    let r = utiles::minmax(zoom as u8);
    Ok(r)
}

#[pyfunction]
pub(crate) fn xyz2quadkey(x: u32, y: u32, z: u8) -> String {
    utiles::xyz2quadkey(x, y, z)
}

#[pyfunction]
pub(crate) fn quadkey2xyz(quadkey: &str) -> PyResult<PyTile> {
    let xyz = utiles::quadkey2tile(quadkey);
    match xyz {
        Ok(xyz) => Ok(PyTile::from(xyz)),
        Err(e) => Err(PyErr::new::<PyValueError, _>(format!("Error: {e}"))),
    }
}

#[pyfunction]
pub(crate) fn qk2xyz(quadkey: &str) -> PyResult<PyTile> {
    quadkey2xyz(quadkey)
}

#[pyfunction]
pub(crate) fn from_tuple(tile: TileTuple) -> PyTile {
    PyTile::py_new(tile.0, tile.1, tile.2)
}

#[pyfunction]
#[pyo3(signature = (tile, fid = None, props = None, projected = None, buffer = None, precision = None)
)]
pub(crate) fn feature<'py>(
    py: Python<'py>,
    tile: PyTileLike,
    // (u32, u32, u8),
    fid: Option<String>,
    props: Option<
        Bound<'py, PyDict>, // HashMap<String, Bound<PyAny>>
    >,
    projected: Option<String>,
    buffer: Option<f64>,
    precision: Option<i32>,
) -> PyResult<Bound<'py, PyDict>> {
    // Convert the arguments to Rust values
    let pytile: PyTile = tile.into();
    let f = pytile.feature(py, fid, props, projected, buffer, precision)?;
    Ok(f)
}

/// Extract a tile or tiles to Vec<PyTile>
///
/// Consolidated logic from `parse_tiles()` per rec by `@nyurikS` in PR #38
pub(crate) fn _extract(arg: &Bound<'_, PyAny>) -> PyResult<Vec<PyTile>> {
    // TODO: this code is identical to parse_tiles() and should be consolidated
    if let Ok(tiles) = arg.extract::<PyTile>() {
        return Ok(vec![tiles]);
    } else if let Ok(tiles) = arg.extract::<Vec<PyTile>>() {
        return Ok(tiles);
    } else if let Ok(seq) = arg.extract::<Vec<(u32, u32, u32)>>() {
        return Ok(seq
            .iter()
            .map(|xyz| PyTile::py_new(xyz.0, xyz.1, xyz.2 as u8))
            .collect());
    } else if let Ok(seq) = arg.extract::<Vec<Vec<u32>>>() {
        return Ok(seq
            .iter()
            .map(|xyz| PyTile::py_new(xyz[0], xyz[1], xyz[2] as u8))
            .collect());
    }
    Err(PyErr::new::<PyValueError, _>(
        "the tile argument may have 1 or 4 values. Note that zoom is a keyword-only argument",
    ))
}

#[pyfunction]
#[pyo3(signature = (*args))]
pub(crate) fn xy_bounds(args: PyTileArg) -> PyBbox {
    let web_bbox = utiles::xyz2bbox(args.x(), args.y(), args.z());
    PyBbox::py_new(
        web_bbox.left(),
        web_bbox.bottom(),
        web_bbox.right(),
        web_bbox.top(),
    )
}

#[pyfunction]
#[pyo3(signature = (lng, lat, zoom, truncate=None))]
pub(crate) fn tile(
    lng: f64,
    lat: f64,
    zoom: u8,
    truncate: Option<bool>,
) -> PyResult<PyTile> {
    if lat <= -90.0 || lat >= 90.0 {
        Err(PyErr::new::<PyValueError, _>(format!(
            "Invalid latitude: {lat}"
        )))?;
    }
    let xyz = utiles::Tile::from_lnglat_zoom(lng, lat, zoom, truncate);
    match xyz {
        Ok(xyz) => Ok(PyTile::from(xyz)),
        Err(e) => Err(PyErr::new::<PyValueError, _>(format!("Error: {e}"))),
    }
}

#[pyfunction]
#[pyo3(signature = (*args))]
pub(crate) fn pmtileid(args: PyTileArg) -> u64 {
    args.pmtileid()
}

#[pyfunction]
pub(crate) fn pmtileid2xyz(pmtileid: u64) -> PyTile {
    let xyz = utiles::Tile::from_pmtileid(pmtileid);
    PyTile::from(xyz)
}

#[pyfunction]
pub(crate) fn from_pmtileid(pmtileid: u64) -> PyTile {
    let xyz = utiles::Tile::from_pmtileid(pmtileid);
    PyTile::from(xyz)
}

#[pyfunction]
#[pyo3(signature = (*args))]
pub(crate) fn quadkey(args: PyTileArg) -> String {
    args.quadkey()
}

#[pyfunction]
pub(crate) fn quadkey_to_tile(quadkey: &str) -> PyResult<PyTile> {
    quadkey2xyz(quadkey)
}

#[pyfunction]
#[pyo3(signature = (*args, zoom = None))]
pub(crate) fn parent(args: PyTileArg, zoom: Option<u8>) -> PyResult<Option<PyTile>> {
    // Parse the tile argument
    let tile = args;
    if tile.z() == 0 {
        return Ok(None);
    }

    // If zoom is not provided, set it to tile.z - 1
    let zoom = zoom.unwrap_or(tile.z() - 1);

    // Check that the zoom level is valid
    if zoom >= tile.z() {
        Err(PyErr::new::<PyValueError, _>(format!(
            "zoom level {} is invalid for tile with zoom {}",
            zoom,
            tile.z()
        )))?;
    }

    let p = utiles::parent(tile.x(), tile.y(), tile.z(), Some(tile.z() - zoom - 1))
        .map(PyTile::from);
    Ok(p)
}

#[pyfunction]
#[pyo3(signature = (*args, zoom = None, zorder = None))]
pub(crate) fn children(
    args: PyTileArg,
    zoom: Option<u8>,
    zorder: Option<bool>,
) -> PyResult<Vec<PyTile>> {
    let zoom = zoom.unwrap_or(args.z() + 1);
    if zoom < args.z() {
        Err(PyErr::new::<PyValueError, _>(format!(
            "zoom must be greater than or equal to tile zoom: {}",
            args.z()
        )))?;
    }
    let children = args.children(Some(zoom), zorder);
    Ok(children)
}

#[pyfunction]
#[pyo3(signature = (*args, zoom = None, wrapx = None))]
pub(crate) fn neighbors(
    args: PyTileArg,
    zoom: Option<u8>,
    wrapx: Option<bool>,
) -> PyResult<Vec<PyTile>> {
    let tile = args;
    let zoom = zoom.unwrap_or(tile.z());
    if zoom < tile.z() {
        Err(PyErr::new::<PyValueError, _>(format!(
            "zoom must be greater than or equal to tile zoom: {}",
            tile.z()
        )))?;
    }
    Ok(tile.neighbors(wrapx))
}

#[pyfunction]
#[pyo3(signature = (*args, truncate = None))]
pub(crate) fn bounding_tile(
    args: &Bound<'_, PyTuple>,
    truncate: Option<bool>,
) -> PyResult<PyTile> {
    let bbox = pyparsing::parse_bbox(args)?;
    // if res.is_err() {
    //     return Err(res.err().unwrap());
    // }
    // let bbox = res;
    let res = utiles::bounding_tile(bbox.into(), truncate)
        .map_err(|e| PyErr::new::<PyValueError, _>(format!("Error: {e}")))?;
    Ok(PyTile::from(res))
}

#[pyfunction]
pub(crate) fn truncate_lnglat(lng: f64, lat: f64) -> (f64, f64) {
    let ll = utiles::LngLat::new(lng, lat);
    let truncated = utiles::truncate_lnglat(&ll);
    (truncated.lng(), truncated.lat())
}

#[pyfunction]
#[pyo3(signature = (west, south, east, north, zooms, truncate=None))]
pub(crate) fn tiles_count(
    west: f64,
    south: f64,
    east: f64,
    north: f64,
    zooms: PyZoomOrZooms,
    truncate: Option<bool>,
) -> PyResult<u64> {
    let (west, south, east, north) =
        utiles::bbox_truncate(west, south, east, north, truncate);

    utiles::tiles_count((west, south, east, north), ZoomOrZooms::from(zooms))
        .map_err(|e| PyErr::new::<PyValueError, _>(format!("Error: {e}")))
}

#[pyfunction]
#[pyo3(signature = (west, south, east, north, zooms, truncate=None))]
pub(crate) fn tiles(
    west: f64,
    south: f64,
    east: f64,
    north: f64,
    zooms: PyZoomOrZooms,
    truncate: Option<bool>,
) -> PyResult<TilesGenerator> {
    let (west, south, east, north) =
        utiles::bbox_truncate(west, south, east, north, truncate);
    let zooms_vec = match zooms {
        PyZoomOrZooms::Zoom(z) => vec![z],
        PyZoomOrZooms::Zooms(zs) => zs,
    };
    let zooms_vec_iter = zooms_vec.clone();
    let ntiles =
        utiles::tiles_count((west, south, east, north), ZoomOrZooms::from(zooms_vec))
            .map_err(|e| PyErr::new::<PyValueError, _>(format!("Error: {e}")))?;
    let xyzs = utiles::tiles(
        (west, south, east, north),
        ZoomOrZooms::from(zooms_vec_iter),
    )
    .map(PyTile::from);
    Ok(TilesGenerator {
        iter: Box::new(xyzs),
        length: ntiles,
    })
}

#[pyfunction]
#[pyo3(signature = (west, south, east, north, zooms, truncate=None))]
pub(crate) fn tiles_list(
    west: f64,
    south: f64,
    east: f64,
    north: f64,
    zooms: PyZoomOrZooms,
    truncate: Option<bool>,
) -> Vec<PyTile> {
    let (west, south, east, north) =
        utiles::bbox_truncate(west, south, east, north, truncate);
    utiles::tiles((west, south, east, north), ZoomOrZooms::from(zooms))
        .map(PyTile::from)
        .collect::<Vec<_>>()
}

#[pyfunction]
pub(crate) fn geotransform2optzoom(geotransform: (f64, f64, f64, f64, f64, f64)) -> u8 {
    utiles::geotransform2optzoom(geotransform)
}

#[pyfunction]
pub(crate) fn geojson_bounds(obj: &Bound<'_, PyAny>) -> PyResult<PyLngLatBbox> {
    let coordsvec = pycoords::coords(obj)?;
    let mut bbox: (f64, f64, f64, f64) = (180.0, 90.0, -180.0, -90.0);

    for (lng, lat) in coordsvec {
        if lat <= -90.0 || lat >= 90.0 {
            Err(PyErr::new::<PyValueError, _>(format!(
                "Invalid latitude: {lat}"
            )))?;
        }
        bbox = (
            bbox.0.min(lng),
            bbox.1.min(lat),
            bbox.2.max(lng),
            bbox.3.max(lat),
        );
    }
    Ok(PyLngLatBbox::py_new(bbox.0, bbox.1, bbox.2, bbox.3))
}
