use anyhow::Result;
use bmp::Image;
use std::{fs::File, io::{self, BufRead, Cursor}};
use rand::prelude::*;
use rayon::prelude::*;

#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
struct Radians(f64);
#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
struct Degrees(f64);

impl From<Degrees> for Radians {
    fn from(value: Degrees) -> Self {
        Radians(value.0 * std::f64::consts::PI / 180f64)
    }
}
impl Into<f64> for Radians {
    fn into(self) -> f64 {
        self.0
    }
}
impl From<Radians> for Degrees {
    fn from(value: Radians) -> Self {
        Degrees(value.0 * (180f64 / std::f64::consts::PI))
    }
}
impl Into<f64> for Degrees {
    fn into(self) -> f64 {
        self.0
    }
}

#[derive(PartialEq, Clone, Debug)]
struct Location {
    lat: Radians,
    lon: Radians,
}
impl From<(Degrees, Degrees)> for Location {
    fn from(value: (Degrees, Degrees)) -> Self {
        Location {
            lat: value.0.into(),
            lon: value.1.into()
        }
    }
}

const IMG_NORTH_BORDER: Radians = Radians(0.9570757299); // Degrees(54.8364);
const IMG_WEST_BORDER:  Radians = Radians(0.2464911049); // Degrees(14.1229);
const IMG_SOUTH_BORDER: Radians = Radians(0.8552532214); // Degrees(49.0024);
const IMG_EAST_BORDER:  Radians = Radians(0.4214184745); // Degrees(24.1455);
const EPSILON: f64 = 0.00001f64;

struct ImageMeta {
    image: Image,
    horizontal_scale: f64,
    vertical_scale: f64
}

#[inline(always)]
fn gcd(a: &Location, b: &Location) -> f64 {
    return f64::acos(
        f64::sin(a.lat.into())*f64::sin(b.lat.into()) + 
        f64::cos(a.lat.into())*f64::cos(b.lat.into())*f64::cos(
            f64::abs(a.lon.0 - b.lon.0)
        )
    )
}

fn dist(location: &Location, shops: &Vec<Location>) -> f64 {
    shops.into_iter().map(|a| {
        gcd(a, &location)
    }).min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Greater)).unwrap()
}

fn in_poland(
    location: &Location, 
    im: &ImageMeta,
) -> bool {
    if location.lat < IMG_SOUTH_BORDER ||
       location.lat >= IMG_NORTH_BORDER || 
       location.lon < IMG_WEST_BORDER || 
       location.lon >= IMG_EAST_BORDER {
        return false
    }

    let offlat = location.lat.0 - IMG_SOUTH_BORDER.0;
    let offlon = location.lon.0 - IMG_WEST_BORDER.0;
    let ppos: (f64, f64) = (offlon * im.horizontal_scale, offlat * im.vertical_scale);
    let ppos = (ppos.0 as u32, im.image.get_height() - (ppos.1 as u32) - 1);

    return im.image.get_pixel(ppos.0, ppos.1) == bmp::consts::BLACK;
}

fn start_point(
    im: &ImageMeta
) -> Location {
    loop {
        let loc = Location {
            lat: Radians(rand::thread_rng().gen_range(IMG_SOUTH_BORDER.0 .. IMG_NORTH_BORDER.0)),
            lon: Radians(rand::thread_rng().gen_range(IMG_WEST_BORDER.0 .. IMG_EAST_BORDER.0)  )
        };
        if in_poland(&loc, im) {
            return loc;
        }
    }
}

fn get_next_point(curr: Location, im: &ImageMeta, shops: &Vec<Location>) -> (f64, Location) {
    [
        Location {lat: Radians(curr.lat.0 + EPSILON), lon: Radians(curr.lon.0)},
        Location {lat: Radians(curr.lat.0 - EPSILON), lon: Radians(curr.lon.0)},
        Location {lat: Radians(curr.lat.0), lon: Radians(curr.lon.0 + EPSILON)},
        Location {lat: Radians(curr.lat.0), lon: Radians(curr.lon.0 - EPSILON)},
        Location {lat: Radians(curr.lat.0 + EPSILON), lon: Radians(curr.lon.0 + EPSILON)},
        Location {lat: Radians(curr.lat.0 + EPSILON), lon: Radians(curr.lon.0 - EPSILON)},
        Location {lat: Radians(curr.lat.0 - EPSILON), lon: Radians(curr.lon.0 - EPSILON)},
        Location {lat: Radians(curr.lat.0 - EPSILON), lon: Radians(curr.lon.0 + EPSILON)},
        curr
    ].into_iter().filter(|loc| {
        in_poland(loc, im)
    }).map(|loc| {
        (dist(&loc, shops), loc)
    }).max_by(|x, y| {
        x.0.partial_cmp(&y.0).unwrap()
    }).unwrap()
}

fn find_local_best(start: Location, im: &ImageMeta, shops: &Vec<Location>) -> (f64, Location) {
    let mut curr = start;
    loop {
        let next = get_next_point(curr.clone(), im, shops);
        if curr == next.1 {
            return next;
        }

        curr = next.1;
    }
}

fn search(im: &ImageMeta, shops: &Vec<Location>) -> (Location, f64) {
    let mut best_dist = 0f64;
    let mut best_point = Location {lat: Radians(0f64), lon: Radians(0f64)};
    for iter in 0..3000 {
        let start = start_point(im);
        let local = find_local_best(start, im, shops);
        if local.0 > best_dist {
            best_dist = local.0;
            best_point = local.1.clone();
            println!("At iter {}, new best", iter);
        }
    }

    return (best_point, best_dist);
}

fn main() -> Result<()> {
    let file = File::open("./pos.txt")?;
    let mut frog_shops: Vec<Location> = Vec::with_capacity(10778);
    for line in io::BufReader::new(file).lines() {
        let line = line?;
        let numbers: Vec<f64> = line.split(" ").map(str::parse::<f64>).map(Result::unwrap).collect();
        frog_shops.push(Location {
            lat: Degrees(numbers[0]).into(), 
            lon: Degrees(numbers[1]).into()
        });
    }

    let poland = include_bytes!("../poland.bmp");
    let mut cursor = Cursor::new(poland);
    let img = bmp::from_reader(&mut cursor)?;
    // println!("{:?}", img);

    // println!("{:?}", dist((
    //     radians!(52f64), radians!(19f64)
    // ), &frog_shops));

    // println!("{:?}", in_poland((radians!(52f64), radians!(19f64)), &img, p_horiz_scale, p_verti_scale));
    // println!("{:?}", degrees_but_tuple!(start_point(&img, p_horiz_scale, p_verti_scale)));
    // println!("{:?}", degrees_but_tuple!(find_local_best(
    //     (radians!(52f64), radians!(19f64)), &img, p_horiz_scale, p_verti_scale, &frog_shops
    // )));
    let imagemeta = ImageMeta {
        horizontal_scale: (img.get_width() as f64) / (IMG_EAST_BORDER.0 - IMG_WEST_BORDER.0),
        vertical_scale: (img.get_height() as f64) / (IMG_NORTH_BORDER.0 - IMG_SOUTH_BORDER.0),
        image: img
    };
    // let result = (0..100)
    //     .into_iter()
    //     .into_par_iter()
    //     .map(|_| search(&imagemeta, &frog_shops))
    //     .max_by(|a, b| {
    //         a.1.partial_cmp(&b.1).unwrap()
    //     }).unwrap();
    let result = search(&imagemeta, &frog_shops);

    let best_point: (Degrees, Degrees) = (result.0.lat.into(), result.0.lon.into());
    println!("Found best point at ({}, {}); Distance is {}", best_point.0.0, best_point.1.0, result.1);
    
    Ok(())
}
