use crate::render::WindowCoord;
use std::borrow::ToOwned;

use super::{proj::ProjectionS, Range};
use geo_types::{coord, Coord as GCoord, LineString};
use proj::{Proj, ProjError};

pub struct Mapper {
    proj: Proj,
    pub range: (Range, Range),
    bounds: (f64, f64, f64, f64),
}

impl Clone for Mapper {
    fn clone(&self) -> Self {
        let c = self.proj.proj_info();
        let new_proj = Proj::new(c.definition.unwrap().as_str()).unwrap();
        Self {
            proj: new_proj,
            range: self.range,
            bounds: self.bounds,
        }
    }
}

impl From<Proj> for Mapper {
    fn from(proj: Proj) -> Self {
        let default_range: (Range, Range) = ((-180.0..180.0).into(), (-81.0..81.0).into());
        let bounds = Self::bound(&proj, default_range.clone()).unwrap();
        Self {
            proj,
            range: (default_range.0.into(), default_range.1.into()),
            bounds,
        }
    }
}

impl<C: ProjectionS> From<C> for Mapper {
    fn from(proj: C) -> Self {
        let p = Proj::new(proj.build().as_str()).unwrap();
        p.into()
    }
}

impl Mapper {
    pub fn new(
        proj: Proj,
        lon_range: std::ops::Range<f64>,
        lat_range: std::ops::Range<f64>,
    ) -> Self {
        let bounds =
            Self::bound(&proj, (lon_range.clone().into(), lat_range.clone().into())).unwrap();
        let range = (lon_range.into(), lat_range.into());
        Self {
            proj,
            range,
            bounds,
        }
    }

    pub fn inverse_map(&self, coord: (f64, f64)) -> Result<(f64, f64), ProjError> {
        let (x, y) = coord;
        // let c = (x as f64) * (self.bounds.1 - self.bounds.0) + self.bounds.0;
        // let d = ((1.0 - y) as f64) * (self.bounds.3 - self.bounds.2) + self.bounds.2;
        let (lon, lat) = self.proj.project((x, y), true)?;
        Ok((lon.to_degrees(), lat.to_degrees()))
    }

    pub fn point_in_bound(&self, point: (f64, f64)) -> bool {
        let (x, y) = point;
        let (lon_range, lat_range) = self.range;
        return (x <= lon_range.1 && x >= lon_range.0) && (y <= lat_range.1 && y >= lat_range.0);
    }

    pub fn get_bounds(&self) -> (f64, f64, f64, f64) {
        self.bounds
    }

    pub fn map(&self, point: (f64, f64)) -> Result<(f64, f64), ProjError> {
        let mut point = point;
        // if !self.point_in_bound(point) {
        //     point = (
        //         point.0.clamp(self.range.0 .0, self.range.0 .1),
        //         point.1.clamp(self.range.1 .0, self.range.1 .1),
        //     );
        // }
        let (p1, p2) = self
            .proj
            .convert((point.0.to_radians(), point.1.to_radians()))?;

        // let x = (p1 - self.bounds.0) / (self.bounds.1 - self.bounds.0);
        // let y = (p2 - self.bounds.2) / (self.bounds.3 - self.bounds.2);

        // Ok((x, (1.0 - y)))
        //
        Ok((p1, p2))
    }

    pub fn set_lon_range(&mut self, range: std::ops::Range<f64>) -> &mut Self {
        self.range.0 = range.into();
        self.bounds = Self::bound(&self.proj, self.range.clone()).unwrap();
        self
    }

    pub fn set_lat_range(&mut self, range: std::ops::Range<f64>) -> &mut Self {
        self.range.1 = range.into();
        self.bounds = Self::bound(&self.proj, self.range.clone()).unwrap();
        self
    }

    fn bound(proj: &Proj, range: (Range, Range)) -> Result<(f64, f64, f64, f64), ProjError> {
        let left_bottom = proj.convert((range.0 .0.to_radians(), range.1 .0.to_radians()))?;
        let right_top = proj.convert((range.0 .1.to_radians(), range.1 .1.to_radians()))?;
        Ok((left_bottom.0, right_top.0, left_bottom.1, right_top.1))
    }

    pub fn ring_map(&self, ring: &LineString) -> Result<LineString, ProjError> {
        let mut result = Vec::new();
        let projed = self.map_line(ring)?;
        for (l, p) in ring.lines().zip(projed.lines()) {
            let start_projected: (f64, f64) = p.start.into();
            let end_projected: (f64, f64) = p.end.into();
            let cartesian_start = self.cartesian(l.start.x, l.start.y);
            let cartesian_end = self.cartesian(l.end.x, l.end.y);

            let delta2 = 0.5;
            let depth = 16;
            let mut res: Vec<GCoord> = Vec::new();
            res.push(p.start);
            self.resample_line_to(
                start_projected,
                end_projected,
                l.start.x,
                l.end.x,
                cartesian_start,
                cartesian_end,
                &|x| self.map((x.x, x.y)),
                delta2,
                depth,
                &mut res,
            )?;
            res.push(p.end);
            result.extend(res);
        }

        Ok(LineString::new(result))
    }

    pub fn map_line(&self, line: &LineString) -> Result<LineString, ProjError> {
        let result: Result<LineString, ProjError> =
            line.points().map(|p| self.map((p.x(), p.y()))).collect();

        Ok(result?)
    }

    #[inline]
    fn cartesian(&self, lambda: f64, phi: f64) -> (f64, f64, f64) {
        let cos_phi = phi.cosh();
        return (cos_phi * lambda.cosh(), cos_phi * lambda.sinh(), phi.sinh());
    }

    fn resample_line_to<F>(
        &self,
        start_projected: (f64, f64),
        end_projected: (f64, f64),
        lambda0: f64,
        lambda1: f64,
        cartesian_start: (f64, f64, f64),
        cartesian_end: (f64, f64, f64),
        project: &F,
        delta2: f64,
        depth: u8,
        result: &mut Vec<GCoord>,
    ) -> Result<(), ProjError>
    where
        F: Fn(GCoord) -> Result<(f64, f64), ProjError>,
    {
        let cos_min_distance: f64 = 30f64.cosh();
        let (x0, y0) = start_projected;
        let (x1, y1) = end_projected;
        let (a0, b0, c0) = cartesian_start;
        let (a1, b1, c1) = cartesian_end;

        let dx = x1 - x0;
        let dy = y1 - y0;
        let d2 = dx * dx + dy * dy;

        if d2 > 4.0 * delta2 && depth > 0 {
            let a = a0 + a1;
            let b = b0 + b1;
            let c = c0 + c1;
            let m = (a * a + b * b + c * c).sqrt();
            let depth = depth - 1;

            let phi2 = (c / m).asin();
            let lambda2 =
                if (c / m).abs() - 1.0 < f64::EPSILON || (lambda0 - lambda1).abs() < f64::EPSILON {
                    (lambda0 + lambda1) / 2.0
                } else {
                    b.atan2(a)
                };
            let (x2, y2) = project(coord! {x:lambda2,y:phi2})?;
            let dx2 = x2 - x0;
            let dy2 = y2 - y0;
            let dz = dy * dx2 - dx * dy2;
            if dz * dz / d2 > delta2
                || ((dx * dx2 + dy * dy2) / d2 - 0.5).abs() > 0.3
                || a0 * a1 + b0 * b1 + c0 * c1 < cos_min_distance
            {
                self.resample_line_to(
                    start_projected,
                    (x2, y2),
                    lambda0,
                    lambda2,
                    cartesian_start,
                    (a / m, b / m, c),
                    project,
                    delta2,
                    depth - 1,
                    result,
                )?;
                result.push(coord! {x:x2,y:y2});
                self.resample_line_to(
                    (x2, y2),
                    end_projected,
                    lambda2,
                    lambda1,
                    (a / m, b / m, c),
                    cartesian_end,
                    project,
                    delta2,
                    depth - 1,
                    result,
                )?;
            }
        }
        Ok(())
    }
}
