use crate::config::Grib2Source;
use crate::pixmap::Pixmap;
use crate::AppState;
use actix_web::error::UrlencodedError::ContentType;
use actix_web::http::StatusCode;
use actix_web::web::{Data, Query};
use actix_web::HttpResponse;
use flate2::read::GzDecoder;
use image::{ImageFormat, ImageReader};
use png::{BitDepth, ColorType, Encoder};
use reqwest::ClientBuilder;
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::f64::consts::PI;
use std::io::{BufWriter, Cursor, Read};
use std::ops::{Add, Div, Mul, Sub};
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::RwLock;
use tracing::{debug, info};
use wxbox_common::TileRequestOptions;
use wxbox_grib2::wgs84::LatLong;
use wxbox_grib2::GribMessage;
use wxbox_pal::{Color, ColorPalette, Palette};

pub async fn needs_reload(
    lct: &RwLock<HashMap<String, SystemTime>>,
    lutkey: &String,
    valid_for: u64,
) -> bool {
    let lct_reader = lct.read().await;

    if let Some(t) = lct_reader.get(lutkey) {
        let dur = SystemTime::now()
            .duration_since(*t)
            .expect("time went backwards")
            .as_secs();
        if dur > valid_for {
            return true;
        }
    } else {
        return true;
    }

    false
}

pub async fn load(url: &str, is_gzipped: bool) -> Vec<u8> {
    let f = reqwest::get(url).await.unwrap();
    let data = f.bytes().await.unwrap().to_vec();

    let out;

    if is_gzipped {
        let b = Cursor::new(data);
        let mut decoder = GzDecoder::new(b);

        let mut data = vec![];
        decoder.read_to_end(&mut data).expect("decoding failure");

        out = data;
    } else {
        out = data;
    }

    out
}

pub async fn reload_if_required(
    from: &str,
    needs_gzip: bool,
    valid_for: u64,
    lut_key: &String,
    lct_cache: &RwLock<HashMap<String, SystemTime>>,
    lut_cache: &RwLock<HashMap<String, Arc<RwLock<GribMessage>>>>,
) {
    if needs_reload(&lct_cache, lut_key, valid_for).await {
        let mut lct_writer = lct_cache.write().await;

        let message = load(from, needs_gzip).await;
        let grib = GribMessage::new(Cursor::new(message)).unwrap();

        lut_cache
            .write()
            .await
            .insert(lut_key.clone(), Arc::new(RwLock::new(grib)));
        lct_writer.insert(lut_key.clone(), SystemTime::now());
    }
}

const TWO_PI: f64 = PI * 2.0;
const HALF_PI: f64 = PI / 2.0;

pub async fn render(
    xtile: f64,
    ytile: f64,
    z: i32,
    tilesize: usize,
    pal: Palette,
    map: &Arc<RwLock<GribMessage>>,
    missing: Option<f64>,
    range_folded: Option<f64>,
    no_coverage: Option<f64>,
    options: &TileRequestOptions,
) -> Pixmap {
    let mut image: Pixmap = Pixmap::new();

    let denominator = 2.0_f64.powi(z) * tilesize as f64;
    let xtile_times_tilesize = xtile * tilesize as f64;
    let ytile_times_tilesize = ytile * tilesize as f64;

    {
        let message = map.read().await;
        for x in 0..tilesize {
            for y in 0..tilesize {
                let tx = (xtile_times_tilesize + x as f64) / denominator;
                let ty = (ytile_times_tilesize + y as f64) / denominator;

                let lon = (TWO_PI * tx - PI).to_degrees();
                let lat = ((PI - TWO_PI * ty).exp().atan() * 2.0_f64 - HALF_PI).to_degrees();

                let nearest = message
                    .value_for(LatLong { lat, long: lon })
                    .map(|u| u as f64);

                let color = match nearest {
                    Some(c) if Some(c) == no_coverage => Color {
                        red: 0,
                        green: 0,
                        blue: 0,
                        alpha: 30,
                    },
                    Some(c) if Some(c) == missing => Color {
                        red: 0,
                        green: 0,
                        blue: 0,
                        alpha: 0,
                    },
                    Some(c) if Some(c) == range_folded => {
                        if options.show_range_folded {
                            let color_raw = options.range_folded_color.to_be_bytes();
                            Color {
                                red: color_raw[0],
                                green: color_raw[1],
                                blue: color_raw[2],
                                alpha: color_raw[3],
                            }
                        } else {
                            Color {
                                red: 0,
                                green: 0,
                                blue: 0,
                                alpha: 0,
                            }
                        }
                    }
                    Some(value_at_pos) => {
                        let mut c = pal.colorize(value_at_pos);
                        if c.red == 0 && c.green == 0 && c.blue == 0 {
                            c.alpha = 0;
                        } else {
                            c.alpha = coloru8(options.data_transparency);
                        }
                        c
                    }
                    None => Color {
                        red: 0,
                        green: 0,
                        blue: 0,
                        alpha: 30,
                    },
                };

                image.set(y, x, color);
            }
        }
    }

    image
}

#[derive(Deserialize)]
struct QueryReq {
    settings: String,
}

fn colorf64(i: u8) -> f64 {
    i as f64 / u8::MAX as f64
}
fn coloru8(i: f64) -> u8 {
    (i * u8::MAX as f64).floor() as u8
}

#[derive(Clone, Copy)]
pub struct ColorF64 {
    pub red: f64,
    pub green: f64,
    pub blue: f64,
    pub alpha: f64,
}
impl From<Color> for ColorF64 {
    fn from(value: Color) -> Self {
        Self {
            red: colorf64(value.red),
            green: colorf64(value.green),
            blue: colorf64(value.blue),
            alpha: colorf64(value.alpha),
        }
    }
}
impl From<ColorF64> for Color {
    fn from(value: ColorF64) -> Self {
        Self {
            red: coloru8(value.red),
            green: coloru8(value.green),
            blue: coloru8(value.blue),
            alpha: coloru8(value.alpha),
        }
    }
}
impl Add for ColorF64 {
    type Output = ColorF64;

    fn add(self, rhs: Self) -> Self::Output {
        Self {
            red: self.red + rhs.red,
            green: self.green + rhs.green,
            blue: self.blue + rhs.blue,
            alpha: self.alpha + rhs.alpha,
        }
    }
}
impl Sub for ColorF64 {
    type Output = ColorF64;

    fn sub(self, rhs: Self) -> Self::Output {
        Self {
            red: self.red - rhs.red,
            green: self.green - rhs.green,
            blue: self.blue - rhs.blue,
            alpha: self.alpha - rhs.alpha,
        }
    }
}
impl Sub<ColorF64> for f64 {
    type Output = ColorF64;

    fn sub(self, rhs: ColorF64) -> Self::Output {
        ColorF64 {
            red: rhs.red - self,
            green: rhs.green - self,
            blue: rhs.blue - self,
            alpha: rhs.alpha - self,
        }
    }
}
impl Mul<f64> for ColorF64 {
    type Output = Self;

    fn mul(self, rhs: f64) -> Self::Output {
        Self {
            red: self.red * rhs,
            blue: self.blue * rhs,
            green: self.green * rhs,
            alpha: self.alpha * rhs,
        }
    }
}
impl Mul for ColorF64 {
    type Output = ColorF64;

    fn mul(self, rhs: Self) -> Self::Output {
        Self {
            red: self.red * rhs.red,
            green: self.green * rhs.green,
            blue: self.blue * rhs.blue,
            alpha: self.alpha * rhs.alpha,
        }
    }
}
impl Div for ColorF64 {
    type Output = ColorF64;

    fn div(self, rhs: Self) -> Self::Output {
        Self {
            red: self.red / rhs.red,
            green: self.green / rhs.green,
            blue: self.blue / rhs.blue,
            alpha: self.alpha / rhs.alpha,
        }
    }
}
impl Div<f64> for ColorF64 {
    type Output = ColorF64;

    fn div(self, rhs: f64) -> Self::Output {
        Self {
            red: self.red / rhs,
            green: self.green / rhs,
            blue: self.blue / rhs,
            alpha: self.alpha / rhs,
        }
    }
}

pub fn merge(base: Pixmap, data: Pixmap) -> Pixmap {
    let mut new = Pixmap::new();

    for x in 0..256 {
        for y in 0..256 {
            let mut c_b: ColorF64 = base.get(x, y).into();
            let mut c_s: ColorF64 = data.get(x, y).into();

            if c_s.red == 0.0 && c_s.green == 0.0 && c_s.blue == 0.0 && c_s.alpha == 0.0 {
                new.set(x, y, c_b.into());
            } else {
                let mut co = (c_s * c_s.alpha + c_b * c_b.alpha * (1.0 - c_s.alpha))
                    / (c_s.alpha + c_b.alpha * (1.0 - c_s.alpha));
                co.alpha = 1.0;

                new.set(x, y, co.into());
            }
        }
    }

    new
}

#[actix_web::get("/{z}/{x}/{y}.png")]
pub async fn source(
    path: actix_web::web::Path<(i32, u32, u32)>,
    req: Query<QueryReq>,
    data: Data<AppState>,
) -> HttpResponse {
    let settings: TileRequestOptions = serde_json::from_str(&req.settings).unwrap();

    // todo: load the base layer from external source
    let base_layer = Pixmap::new();

    let base_layer: Pixmap = if settings.baselayer == "osm" {
        let client = ClientBuilder::new()
            .user_agent(format!("wxbox-tiler/{}", env!("CARGO_PKG_VERSION")))
            .build()
            .unwrap();
        let body = client
            .get(format!(
                "https://tile.openstreetmap.org/{}/{}/{}.png",
                path.0, path.1, path.2
            ))
            .send()
            .await
            .unwrap()
            .bytes()
            .await
            .unwrap();
        let mut img = ImageReader::new(Cursor::new(body.to_vec()));
        img.set_format(ImageFormat::Png);
        let img = img.decode().unwrap();
        let rgb = img.into_rgba8();
        // copy it into a pixmap
        let mut map = Pixmap::new();
        for x in 0..256_usize {
            for y in 0..256_usize {
                let pix = rgb.get_pixel(y as u32, x as u32);
                map.set(
                    x,
                    y,
                    Color {
                        red: pix[0],
                        green: pix[1],
                        blue: pix[2],
                        alpha: pix[3],
                    },
                );
            }
        }
        map
    } else {
        debug!("not found baselayer");
        return HttpResponse::new(StatusCode::NOT_FOUND);
    };

    // data layer
    // valid types:
    // - grib2/

    let data_layer: Pixmap = if settings.data.starts_with("grib2/") {
        if let Some(known_source) = data
            .config
            .sources
            .grib2
            .get(settings.data.strip_prefix("grib2/").unwrap())
        {
            reload_if_required(
                &known_source.from,
                known_source.needs_gzip,
                known_source.valid_for.into(),
                &settings.data,
                &data.grib2_cache_timestamps,
                &data.grib2_cache,
            )
            .await;
            let lct_reader = data.grib2_cache_timestamps.read().await;
            if let Some(grib2) = data.grib2_cache.read().await.get(&settings.data) {
                crate::sources::grib2::render(
                    path.1 as f64,
                    path.2 as f64,
                    path.0,
                    256,
                    wxbox_pal::parser::parse(&known_source.palette).unwrap(),
                    grib2,
                    known_source.missing,
                    known_source.range_folded,
                    known_source.no_coverage,
                    &settings,
                )
                .await
            } else {
                debug!("not found grib2 after reload in base cache");
                return HttpResponse::new(StatusCode::NOT_FOUND);
            }
        } else {
            debug!("not found grib2 in configuration");
            return HttpResponse::new(StatusCode::NOT_FOUND);
        }
    } else {
        debug!("not found datalayer registry");
        return HttpResponse::new(StatusCode::NOT_FOUND);
    };

    let image = merge(base_layer, data_layer);

    let mut buf: Vec<u8> = vec![];
    // borrow checker insanity
    {
        let mut cur: Cursor<_> = Cursor::new(&mut buf);
        let w = &mut BufWriter::new(&mut cur);
        let mut encoder = Encoder::new(w, 256, 256);
        encoder.set_color(ColorType::Rgba);
        encoder.set_depth(BitDepth::Eight);
        encoder.set_source_gamma(png::ScaledFloat::from_scaled(45455));
        encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2));
        let source_chromaticities = png::SourceChromaticities::new(
            (0.31270, 0.32900),
            (0.64000, 0.33000),
            (0.30000, 0.60000),
            (0.15000, 0.06000),
        );
        encoder.set_source_chromaticities(source_chromaticities);
        let mut writer = encoder.write_header().unwrap();
        writer
            .write_image_data(&image.to_raw())
            .expect("failed to encode png");
        writer.finish().unwrap();
    }

    HttpResponse::Ok()
        .insert_header(actix_web::http::header::ContentType(mime::IMAGE_PNG))
        // TODO: use the timestamp in the grib2 ID section
        //.insert_header(("x-wxbox-tiler-data-valid-time", lct_reader.get(&settings.data).expect("impossible").duration_since(::std::time::UNIX_EPOCH).expect("time went backwards").as_secs().to_string()))
        .insert_header(("Access-Control-Allow-Origin", "*"))
        .insert_header(("Access-Control-Expose-Headers", "*"))
        .insert_header(("Access-Control-Allow-Headers", "*"))
        .body(buf)
}
/*
#[macro_export]
macro_rules! grib2_handler {
    (mount $f:ident, at: $path:expr, from: $from:expr, needs_gzip: $needs_gzip:expr, valid_for: $valid_for:expr, palette: $pal:expr, missing: $missing:expr, range_folded: $rf:expr, no_coverage: $nc:expr) => {
        #[::actix_web::get($path)]
        async fn $f(path: ::actix_web::web::Path<(i32,u32,u32)>, data: ::actix_web::web::Data<crate::AppState>) -> ::actix_web::HttpResponse {
            crate::sources::grib2::reload_if_required(
                $from,
                $needs_gzip,
                $valid_for,
                &String::from($path),
                &data.grib2_cache_timestamps,
                &data.grib2_cache
            ).await;
            let lct_reader = data.grib2_cache_timestamps.read().await;
            if let Some(map) = data.grib2_cache.read().await.get($path) {
                ::actix_web::HttpResponse::Ok()
                    .insert_header(::actix_web::http::header::ContentType(::mime::IMAGE_PNG))
                    .insert_header(("x-wxbox-tiler-data-valid-time", lct_reader.get($path).expect("impossible").duration_since(::std::time::UNIX_EPOCH).expect("time went backwards").as_secs().to_string()))
                    .insert_header(("Access-Control-Allow-Origin", "*"))
                    .insert_header(("Access-Control-Expose-Headers", "*"))
                    .insert_header(("Access-Control-Allow-Headers", "*"))
                    .body(crate::sources::grib2::render(path.1 as f64, path.2 as f64, path.0, 256, $pal, map, $missing, $rf, $nc).await)
            } else {
                ::actix_web::HttpResponse::new(::actix_web::http::StatusCode::NOT_FOUND)
            }
        }
    };
}*/
