use std::fmt;
use std::time::SystemTime;

use actix_web::http;
use actix_web::middleware;
use actix_web::server;
use actix_web::App;
use actix_web::HttpRequest;
use actix_web::HttpResponse;
use actix_web::Path;
use actix_web::Query;
use actix_web::Result;
use askama::Template;
use image;
use log::info;
use rand::SeedableRng;
use rand_xorshift::XorShiftRng;
use serde_derive::Deserialize;
use structopt::StructOpt;

use renderer::noise;
use renderer::noise::lode::Lode;
use renderer::noise::perlin::Perlin;
use renderer::noise::NoiseType;
use renderer::texture::NoiseTexture;
use renderer::texture::Texture;
use renderer::vec3::Vec3;

#[derive(Debug, StructOpt)]
#[structopt(name = "noise_explorer", about = "CLI for exploring Perlin noise")]
struct Opt {
    /// HTTP listen address
    #[structopt(long = "addr", default_value = "0.0.0.0:8889")]
    pub addr: String,

    /// Width of noise images
    #[structopt(short = "w", long = "width", default_value = "128")]
    pub width: u32,
    /// Height of noise images
    #[structopt(short = "h", long = "height", default_value = "128")]
    pub height: u32,
}

#[derive(Copy, Clone, Debug, Deserialize, PartialEq)]
enum NoiseSource {
    #[serde(rename = "perlin")]
    Perlin,
    #[serde(rename = "lode")]
    Lode,
}

impl fmt::Display for NoiseSource {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            NoiseSource::Perlin => write!(f, "perlin"),
            NoiseSource::Lode => write!(f, "lode"),
        }
    }
}

#[derive(Copy, Clone, Debug, Deserialize)]
struct OptionalParams {
    pixel_scale: f32,
}

#[derive(Debug, Deserialize)]
struct NoiseSourceParam {
    noise_source: NoiseSource,
}

#[derive(Debug, Deserialize)]
struct NoiseParams {
    width: u32,
    height: u32,
    noise_source: NoiseSource,
    noise_type: NoiseType,
    optional: Option<OptionalParams>,
}

#[derive(Debug, Deserialize)]
struct NoiseParamsScale {
    width: u32,
    height: u32,
    noise_source: NoiseSource,
    scale: f32,
}

#[derive(Debug, Deserialize)]
struct NoiseParamsTurbulence {
    width: u32,
    height: u32,
    noise_source: NoiseSource,
    turbulence: usize,
}

#[derive(Debug, Deserialize)]
struct NoiseParamsMarble {
    width: u32,
    height: u32,
    noise_source: NoiseSource,
    x: f32,
    y: f32,
    z: f32,
    power: f32,
    size: usize,
    scale: f32,
}

impl NoiseParams {
    fn url(&self) -> String {
        let mut s = format!(
            "/noise/{}/{}/{}/{}",
            self.noise_source,
            self.width,
            self.height,
            self.noise_type.to_url(),
        );
        if let Some(optional) = self.optional {
            s += &format!("?pixel_scale={}", optional.pixel_scale);
        };
        s
    }

    fn big_url(&self) -> String {
        let mut s = format!(
            "/noise/{}/1024/1024/{}",
            self.noise_source,
            self.noise_type.to_url()
        );
        if let Some(optional) = self.optional {
            s += &format!("?pixel_scale={}", optional.pixel_scale);
        };
        s
    }
}

fn render_noise(noise_params: NoiseParams) -> image::GrayImage {
    let options = noise_params
        .optional
        .unwrap_or(OptionalParams { pixel_scale: 1.0 });
    const SEED: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
    let mut rng: XorShiftRng = SeedableRng::from_seed(SEED);
    let mut img = image::GrayImage::new(noise_params.width, noise_params.height);
    let tex: NoiseTexture<Box<dyn noise::NoiseSource>> = match noise_params.noise_source {
        NoiseSource::Perlin => {
            NoiseTexture::new(Box::new(Perlin::new(&mut rng)), noise_params.noise_type)
        }
        NoiseSource::Lode => {
            NoiseTexture::new(Box::new(Lode::new(&mut rng)), noise_params.noise_type)
        }
    };
    info!("{:?}", noise_params);

    for x in 0..img.width() {
        for y in 0..img.height() {
            let u = x as f32 / options.pixel_scale;
            let v = y as f32 / options.pixel_scale;
            let p = Vec3::new(u, v, 1.);
            let luma = tex.value(u, v, p).x;
            let luma = (luma * 255.) as u8;
            img.put_pixel(x, y, image::Luma([luma]));
        }
    }

    img
}

struct Specimen {
    title: String,
    params: Vec<NoiseParams>,
}

#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {
    render_time: u64,
    noise_source: NoiseSource,
    specimens: Vec<Specimen>,
}

fn render_html(
    noise_source: NoiseSource,
    specimens: Vec<Specimen>,
) -> Result<String, askama::Error> {
    let index = IndexTemplate {
        render_time: SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap()
            .as_secs(),
        noise_source,
        specimens,
    };
    index.render()
}

fn build_specimens(
    noise_source: NoiseSource,
    width: u32,
    height: u32,
    optional: Option<OptionalParams>,
) -> Vec<Specimen> {
    let mut specimens = Vec::new();
    let mut specimen = Specimen {
        title: "Unperturbed".into(),
        params: Vec::new(),
    };
    {
        let params = NoiseParams {
            width,
            height,
            noise_source,
            noise_type: NoiseType::Scale(1.),
            optional,
        };
        specimen.params.push(params);
    }
    specimens.push(specimen);

    let mut specimen = Specimen {
        title: "Varying Scale Factor <1".into(),
        params: Vec::new(),
    };
    let debug = false;
    if !debug {
        for scale in 1..10 {
            let params = NoiseParams {
                width,
                height,
                noise_source,
                noise_type: NoiseType::Scale(scale as f32 / 10.),
                optional,
            };
            specimen.params.push(params);
        }
        specimens.push(specimen);

        let mut specimen = Specimen {
            title: "Varying Scale Factor >1".into(),
            params: Vec::new(),
        };
        for scale in 1..10 {
            let params = NoiseParams {
                width,
                height,
                noise_source,
                noise_type: NoiseType::Scale(scale as f32),
                optional,
            };
            specimen.params.push(params);
        }
        specimens.push(specimen);

        let mut specimen = Specimen {
            title: "Varying Turbulence Factor".into(),
            params: Vec::new(),
        };
        for turbulence in 0..9 {
            let params = NoiseParams {
                width,
                height,
                noise_source,
                noise_type: NoiseType::Turbulence(1 << turbulence),
                optional,
            };
            specimen.params.push(params);
        }
        specimens.push(specimen);

        let max_size = 8;
        for power in 1..12 {
            let mut specimen = Specimen {
                title: format!(
                    "Marble power {}, varying size 2-{}",
                    power,
                    1 << (max_size - 1)
                ),
                params: Vec::new(),
            };
            for size in 1..max_size {
                let params = NoiseParams {
                    width,
                    height,
                    noise_source,
                    noise_type: NoiseType::Marble {
                        period: Vec3::new(5., 10., 0.),
                        power: power as f32,
                        size: 1 << size,
                        scale: 1.,
                    },
                    optional,
                };
                specimen.params.push(params);
            }
            specimens.push(specimen);
        }

        let mut specimen = Specimen {
            title: "Marble varying power, size = 32".into(),
            params: Vec::new(),
        };
        for power in 1..10 {
            let params = NoiseParams {
                width,
                height,
                noise_source,
                noise_type: NoiseType::Marble {
                    period: Vec3::new(0., 1., 0.),
                    power: power as f32,
                    size: 32,
                    scale: 1.,
                },
                optional,
            };
            specimen.params.push(params);
        }
        specimens.push(specimen);
        let mut specimen = Specimen {
            title: "Marble varying scale, size = 32".into(),
            params: Vec::new(),
        };
        for scale in (1..4).rev() {
            let params = NoiseParams {
                width,
                height,
                noise_source,
                noise_type: NoiseType::Marble {
                    period: Vec3::new(0., 1., 0.),
                    power: 1.,
                    size: 32,
                    scale: 1. / (1 << scale) as f32,
                },
                optional,
            };
            specimen.params.push(params);
        }
        for scale in 1..4 {
            let params = NoiseParams {
                width,
                height,
                noise_source,
                noise_type: NoiseType::Marble {
                    period: Vec3::new(0., 1., 0.),
                    power: 1.,
                    size: 32,
                    scale: (1 << scale) as f32,
                },
                optional,
            };
            specimen.params.push(params);
        }
        specimens.push(specimen);
    }
    specimens
}

fn style(_req: &HttpRequest) -> Result<HttpResponse> {
    let bytes = include_bytes!("../templates/style.css");
    Ok(HttpResponse::Ok().content_type("text/css").body(&bytes[..]))
}

fn index(source: NoiseSource, optional: Option<Query<OptionalParams>>) -> Result<HttpResponse> {
    let width = 128;
    let height = 128;
    let specimens = build_specimens(source, width, height, optional.map(|o| o.into_inner()));
    Ok(HttpResponse::Ok()
        .content_type("text/html")
        .body(render_html(source, specimens).unwrap()))
}

fn index_perlin(optional: Option<Query<OptionalParams>>) -> Result<HttpResponse> {
    index(NoiseSource::Perlin, optional)
}

fn index_lode(optional: Option<Query<OptionalParams>>) -> Result<HttpResponse> {
    index(NoiseSource::Lode, optional)
}

fn noise(np: NoiseParams) -> Result<HttpResponse> {
    let img = render_noise(np);
    let mut buf = Vec::new();
    let img = image::DynamicImage::ImageLuma8(img);
    // TODO(wathiede): remove unwrap with proper error propagation.
    img.write_to(&mut buf, image::ImageOutputFormat::PNG)
        .unwrap();
    Ok(HttpResponse::Ok().content_type("image/png").body(buf))
}

#[allow(clippy::needless_pass_by_value)]
fn noise_scale(
    np: Path<NoiseParamsScale>,
    optional: Option<Query<OptionalParams>>,
) -> Result<HttpResponse> {
    noise(NoiseParams {
        width: np.width,
        height: np.height,
        noise_source: np.noise_source,
        noise_type: NoiseType::Scale(np.scale),
        optional: optional.map(|o| o.into_inner()),
    })
}

#[allow(clippy::needless_pass_by_value)]
fn noise_turbulence(
    np: Path<NoiseParamsTurbulence>,
    optional: Option<Query<OptionalParams>>,
) -> Result<HttpResponse> {
    noise(NoiseParams {
        width: np.width,
        height: np.height,
        noise_source: np.noise_source,
        noise_type: NoiseType::Turbulence(np.turbulence),
        optional: optional.map(|o| o.into_inner()),
    })
}

#[allow(clippy::needless_pass_by_value)]
fn noise_marble(
    np: Path<NoiseParamsMarble>,
    optional: Option<Query<OptionalParams>>,
) -> Result<HttpResponse> {
    noise(NoiseParams {
        width: np.width,
        height: np.height,
        noise_source: np.noise_source,
        noise_type: NoiseType::Marble {
            period: Vec3::new(np.x, np.y, np.z),
            power: np.power,
            size: np.size,
            scale: np.scale,
        },
        optional: optional.map(|o| o.into_inner()),
    })
}

fn main() -> Result<(), std::io::Error> {
    stderrlog::new()
        .verbosity(2)
        .timestamp(stderrlog::Timestamp::Millisecond)
        .init()
        .unwrap();
    let opt = Opt::from_args();

    server::new(|| {
        App::new()
            .resource("/noise/{noise_source}/{width}/{height}/scale/{scale}", |r| {
                r.with(noise_scale);
            }).resource("/noise/{noise_source}/{width}/{height}/turbulence/{turbulence}", |r| {
                r.with(noise_turbulence);
            }).resource(
                "/noise/{noise_source}/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}/scale/{scale}",
                |r| {
                    r.with(noise_marble);
                },
            ).resource("/style.css", |r| r.f(style))
            .resource("/lode", |r| r.with(index_lode))
            .resource("/perlin", |r| r.with(index_perlin))
            .resource("/", |r| r.f(|_| HttpResponse::Found().header(http::header::LOCATION, "/lode").finish()))
            .middleware(middleware::Logger::default())
    }).bind(opt.addr)
    .unwrap()
    .run();
    Ok(())
}

#[cfg(test)]
mod tests {
    use std::str;

    use actix_web::http::Method;
    use actix_web::test::TestServer;
    use actix_web::HttpMessage;
    use actix_web::Path;
    use actix_web::Query;

    use super::NoiseParamsMarble;
    use super::NoiseParamsScale;
    use super::NoiseParamsTurbulence;
    use super::OptionalParams;

    #[test]
    fn noise_param_from_req() {
        let mut srv = TestServer::build().start(|app| {
            app.resource("/noise/{noise_source}/{width}/{height}/scale/{scale}", |r| {
                r.with(|p: Path<NoiseParamsScale>, q:Option<Query<OptionalParams>>| format!("{:?} {:?}", p,q));
            });
            app.resource("/noise/{noise_source}/{width}/{height}/turbulence/{turbulence}", |r| {
                r.with(|p: Path<NoiseParamsTurbulence>| format!("{:?}", p));
            });
            app.resource(
                "/noise/{noise_source}/{width}/{height}/marble/period/{x},{y},{z}/power/{power}/size/{size}/scale/{scale}",
                |r| {
                    r.with(|p: Path<NoiseParamsMarble>| format!("{:?}", p));
                },
            );
        });

        let request = srv
            .client(Method::GET, "/noise/perlin/32/32/scale/1")
            .finish()
            .unwrap();
        let response = srv.execute(request.send()).unwrap();
        assert!(response.status().is_success(), "Response {:?}", response);

        let bytes = srv.execute(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        assert_eq!(
            body,
            "NoiseParamsScale { width: 32, height: 32, noise_source: Perlin, scale: 1.0 } None"
        );

        let request = srv
            .client(Method::GET, "/noise/perlin/32/32/scale/1?pixel_scale=1")
            .finish()
            .unwrap();
        let response = srv.execute(request.send()).unwrap();
        assert!(response.status().is_success(), "Response {:?}", response);

        let bytes = srv.execute(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        assert_eq!(
            body,
            "NoiseParamsScale { width: 32, height: 32, noise_source: Perlin, scale: 1.0 } Some(OptionalParams { pixel_scale: 1.0 })"
        );

        let request = srv
            .client(Method::GET, "/noise/lode/32/32/scale/1")
            .finish()
            .unwrap();
        let response = srv.execute(request.send()).unwrap();
        assert!(response.status().is_success(), "Response {:?}", response);

        let bytes = srv.execute(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        assert_eq!(
            body,
            "NoiseParamsScale { width: 32, height: 32, noise_source: Lode, scale: 1.0 } None"
        );

        let request = srv
            .client(Method::GET, "/noise/lode/32/32/turbulence/5")
            .finish()
            .unwrap();
        let response = srv.execute(request.send()).unwrap();
        assert!(response.status().is_success(), "Response {:?}", response);

        let bytes = srv.execute(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        assert_eq!(
            body,
            "NoiseParamsTurbulence { width: 32, height: 32, noise_source: Lode, turbulence: 5 }"
        );

        let request = srv
            .client(
                Method::GET,
                "/noise/lode/32/32/marble/period/1.,2.,3./power/32./size/4/scale/255.",
            )
            .finish()
            .unwrap();
        let response = srv.execute(request.send()).unwrap();
        assert!(response.status().is_success(), "Response {:?}", response);

        let bytes = srv.execute(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        assert_eq!(
            body,
            "NoiseParamsMarble { width: 32, height: 32, noise_source: Lode, x: 1.0, y: 2.0, z: 3.0, power: 32.0, size: 4, scale: 255.0 }"
        );
    }
}
