use crate::proxy::HSTS;
use crate::Config;
use axum::extract::{Path, State};
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use hyper;
use hyper::header::*;
use hyper::StatusCode;
use rand::distr::StandardUniform;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaChaRng;
use serde_derive::*;
use std;
use std::f64::consts::SQRT_2;
use std::io::Write;
use tracing::*;

const N_POINTS: usize = 10;
const MARGIN: f64 = 10.;
const SCALE: f64 = 200.;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Size {
    Full,
    Small,
}

const QUESTION: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-question-circle" viewBox="1 1 14 14">
  <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
  <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
</svg>"#;

pub fn identicon() -> axum::Router<Config> {
    axum::Router::new()
        .route("/{login}", axum::routing::get(identicon_full))
        .route("/{login}/small", axum::routing::get(identicon_small))
}

#[derive(Debug, Deserialize)]
struct Identicon {
    login: String,
}

type Body = http_body_util::Full<bytes::Bytes>;

#[axum::debug_handler]
async fn identicon_full(
    config: State<Config>,
    name: Path<Identicon>,
    req: axum::extract::Request,
) -> hyper::Response<Body> {
    identicon_(Size::Full, config, name, req).await.unwrap()
}

#[axum::debug_handler]
async fn identicon_small(
    config: State<Config>,
    name: Path<Identicon>,
    req: axum::extract::Request,
) -> hyper::Response<Body> {
    identicon_(Size::Small, config, name, req).await.unwrap()
}

async fn identicon_(
    size: Size,
    State(config): State<Config>,
    Path(name): Path<Identicon>,
    req: axum::extract::Request,
    /*
    name: String,
    req: &hyper::Request<hyper::body::Body>,
    */
) -> Result<hyper::Response<Body>, crate::Error> {
    use crate::db::users::dsl as users;
    let mut db_ = config.db.get().await.unwrap();
    let uid = if let Some(user) = users::users
        .filter(users::login.eq(&name.login))
        .select(users::id)
        .get_result::<uuid::Uuid>(&mut db_)
        .await
        .optional()
        .unwrap()
    {
        user
    } else {
        if let Some(ifmodified) = req
            .headers()
            .get(IF_MODIFIED_SINCE)
            .and_then(|ifmodified| ifmodified.to_str().ok())
            .and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
        {
            if config.version_time <= ifmodified + std::time::Duration::from_secs(1) {
                let now = std::time::SystemTime::now();
                return Ok(hyper::Response::builder()
                    .status(StatusCode::NOT_MODIFIED)
                    .header(STRICT_TRANSPORT_SECURITY, HSTS)
                    .header(LAST_MODIFIED, &config.version_time_str)
                    .header(DATE, httpdate::fmt_http_date(now))
                    .body("".into())?);
            }
        }
        return Ok(hyper::Response::builder()
            .status(StatusCode::OK)
            .header(LAST_MODIFIED, config.version_time_str.as_str())
            .header(STRICT_TRANSPORT_SECURITY, HSTS)
            .header(CONTENT_TYPE, "image/svg+xml")
            .body(QUESTION.into())?);
    };
    debug!("id: {:?}", uid);
    use crate::db::profile_pics::dsl as profile_pics;
    if let Some((mime, bytes, modif)) = (if let Size::Full = size {
        profile_pics::profile_pics
            .find(uid)
            .select((
                profile_pics::format,
                profile_pics::image,
                profile_pics::updated,
            ))
            .get_result(&mut db_)
    } else {
        profile_pics::profile_pics
            .find(uid)
            .select((
                profile_pics::format,
                profile_pics::small,
                profile_pics::updated,
            ))
            .get_result::<(String, Vec<u8>, chrono::DateTime<chrono::Utc>)>(&mut db_)
    })
    .await
    .optional()?
    {
        let modif: std::time::SystemTime = modif.into();
        if let Some(ifmodified) = req
            .headers()
            .get(IF_MODIFIED_SINCE)
            .and_then(|ifmodified| ifmodified.to_str().ok())
            .and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
        {
            if modif > ifmodified {
                let now = std::time::SystemTime::now();
                return Ok(hyper::Response::builder()
                    .status(StatusCode::NOT_MODIFIED)
                    .header(STRICT_TRANSPORT_SECURITY, HSTS)
                    .header(LAST_MODIFIED, httpdate::fmt_http_date(modif))
                    .header(DATE, httpdate::fmt_http_date(now))
                    .body("".into())?);
            }
        }
        return Ok(hyper::Response::builder()
            .status(StatusCode::OK)
            .header(STRICT_TRANSPORT_SECURITY, HSTS)
            .header(CONTENT_TYPE, &mime)
            .header(LAST_MODIFIED, httpdate::fmt_http_date(modif))
            .body(bytes.into())?);
    }

    let now = std::time::SystemTime::now();
    if let Some(ifmodified) = req
        .headers()
        .get(IF_MODIFIED_SINCE)
        .and_then(|ifmodified| ifmodified.to_str().ok())
        .and_then(|ifmodified| httpdate::parse_http_date(ifmodified).ok())
    {
        if config.version_time <= ifmodified + std::time::Duration::from_secs(1) {
            return Ok(hyper::Response::builder()
                .status(StatusCode::NOT_MODIFIED)
                .header(STRICT_TRANSPORT_SECURITY, HSTS)
                .header(LAST_MODIFIED, &config.version_time_str)
                .header(DATE, httpdate::fmt_http_date(now))
                .body("".into())?);
        }
    }
    Ok(hyper::Response::builder()
        .status(StatusCode::OK)
        .header(LAST_MODIFIED, config.version_time_str.as_str())
        .header(CONTENT_TYPE, "image/svg+xml")
        .header(STRICT_TRANSPORT_SECURITY, HSTS)
        .header(DATE, httpdate::fmt_http_date(now))
        .body(make_identicon(name.login.as_bytes()).into())?)
}

fn make_identicon(user: &[u8]) -> Vec<u8> {
    let seed = seed_bytes(user);
    let rng: ChaChaRng = SeedableRng::from_seed(seed);

    let mut points = [(0., 0.); N_POINTS];
    for ((x, y), p) in rng
        .sample_iter(&StandardUniform)
        .scan(
            (0., 0.),
            |&mut (ref mut x0, ref mut y0), (x, y): (f64, f64)| {
                let x: f64 = (x * 2.) - 1.;
                let y: f64 = (y * 2.) - 1.;
                let d = (x * x + y * y).sqrt();
                *x0 = *x0 + x / d;
                *y0 = *y0 + y / d;
                Some((*x0, *y0))
            },
        )
        .zip(points.iter_mut())
    {
        *p = (x, y)
    }
    // Compute bounding box.
    let (extr_x, extr_y) = bezier_extr(points[0], points[1], points[2]);
    let mut min_x = points[0].0.min(points[2].0).min(extr_x);
    let mut min_y = points[0].1.min(points[2].1).min(extr_y);
    let mut max_x = points[0].0.max(points[2].0).max(extr_x);
    let mut max_y = points[0].1.max(points[2].1).max(extr_y);

    // Since we're using splines, "mid" is the middle control
    // point in the quadratic Bezier curve, at each step,
    // starting at step one.
    let mut mid = points[1];
    for (&(x0, y0), &(x1, y1)) in points.iter().skip(2).zip(points.iter().skip(3)) {
        mid.0 = x0 + (x0 - mid.0);
        mid.1 = y0 + (y0 - mid.1);

        let (m0, m1) = bezier_extr((x0, y0), mid, (x1, y1));
        min_x = min_x.min(m0).min(x1);
        min_y = min_y.min(m1).min(y1);

        max_x = max_x.max(m0).max(x1);
        max_y = max_y.max(m1).max(y1);
    }

    let max = (max_x - min_x).max(max_y - min_y);
    let scale = SQRT_2 * max;
    let margin_x = (max - (max_x - min_x)) / 2. - min_x;
    let margin_y = (max - (max_y - min_y)) / 2. - min_y;
    for &mut (ref mut x, ref mut y) in points.iter_mut() {
        *x = MARGIN + SCALE * ((1. - 1. / SQRT_2) / 2. + ((margin_x + *x) / scale));
        *y = MARGIN + SCALE * ((1. - 1. / SQRT_2) / 2. + ((margin_y + *y) / scale));
    }

    let mut s:Vec<u8> = format!(r#"<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision"><path d=""#, SCALE + MARGIN * 2., SCALE + MARGIN * 2.).into();

    write!(
        s,
        "M {} {} Q {} {}, {} {} ",
        points[0].0.round(),
        points[0].1.round(),
        points[1].0.round(),
        points[1].1.round(),
        points[2].0.round(),
        points[2].1.round()
    )
    .unwrap();

    for &(x, y) in points.iter().skip(3) {
        write!(s, "T {} {} ", x.round(), y.round()).unwrap();
    }
    /*
        // colored signatures
        let h = rng.gen::<f64>();
        let color = Hsv::new(RgbHue::from(h * 360f64), 0.8, 1.);
        let rgb = Rgb::from(color);
        write!(s, "\" stroke=\"rgb({},{},{})\" stroke-width=\"2\" fill=\"transparent\"/></svg>",
        (rgb.red * 255f64).round() as u8,
        (rgb.green * 255f64).round() as u8,
        (rgb.blue * 255f64).round() as u8
    ).unwrap();
         */
    write!(
        s,
        "\" stroke=\"black\" stroke-width=\"3\" fill=\"transparent\"/></svg>"
    )
    .unwrap();
    debug!("s: {:?}", std::str::from_utf8(&s));
    s
}

fn bezier_extr(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> (f64, f64) {
    (bezier_extr_1(a.0, b.0, c.0), bezier_extr_1(a.1, b.1, c.1))
}

fn bezier_extr_1(a: f64, b: f64, c: f64) -> f64 {
    // Coefficient of the polynomial.
    let u = a - 2. * b + c;
    let v = 2. * (b - a);
    let w = a;
    let t = ((-v) / (2. * u)).min(1.).max(0.);
    u * t * t + v * t + w
}

fn seed_bytes(from: &[u8]) -> [u8; 32] {
    let mut u = [0; 32];
    let l = from.len();
    for i in 0..32 {
        u[i] = from[i % l];
    }
    u
}