/* a port from http://www.color-blindness.com/coblis-color-blindness-simulator/ */

use std::collections::HashMap;
use std::path::PathBuf;

use image::{ImageBuffer, Rgba};
use structopt::StructOpt;

#[derive(StructOpt, Debug)]
struct Opt {
    #[structopt(required = true, name = "FILE", parse(from_os_str))]
    inputs: Vec<PathBuf>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let opt = Opt::from_args();
    for file in &opt.inputs {
        if file.exists() == false {
            eprintln!("input file {:?} does not exists", file.display());
            std::process::exit(1);
        }
    }

    for file in &opt.inputs {
        process(file)?;
    }

    Ok(())
}
fn process(input: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
    let path = format!("{}", input.display());
    let img = image::open(&path)?;
    let (name, extension) = path.split_at(
        path.rfind(".")
            .expect("path does not contain a detectable extension"),
    );

    let filters: Vec<(&str, Box<dyn Fn(&Rgba<u8>) -> Rgba<u8>>)> = vec![
        //("monochrome", Box::new(monochrome)),
        ("Protanopia", Box::new(|p| blindMK(p, "protan"))),
        ("Protanomaly", Box::new(|p| anomylize(p, blindMK(p, "protan")))),
        ("Deuteranopia", Box::new(|p| blindMK(p, "deutan"))),
        ("Deuteranomaly", Box::new(|p| anomylize(p, blindMK(p, "deutan")))),
        ("Tritanopia", Box::new(|p| blindMK(p, "tritan"))),
        ("Tritanomaly", Box::new(|p| anomylize(p, blindMK(p, "tritan")))),
        ("Achromatopsia", Box::new(|p| monochrome(p))),
        ("Achromatomaly", Box::new(|p| anomylize(p, monochrome(p)))),
    ];

    for (i, (label, func)) in filters.iter().enumerate() {
        let out_filename = format!("{}_{}-{}{}", name, i, label, extension);
        print!("preparing {:?} ... ", out_filename);
        flush()?;
        let mut buffer: ImageBuffer<_, _> = img.to_rgba();

        for (_x, _y, pixel) in buffer.enumerate_pixels_mut() {
            *pixel = func(pixel);
        }

        print!("saving ... ");
        flush()?;
        buffer.save(out_filename)?;
        println!("done");
    }

    Ok(())
}
fn flush() -> std::io::Result<()> {
    use std::io::Write;
    std::io::stdout().flush()
}

fn monochrome(pixel: &Rgba<u8>) -> Rgba<u8> {
    let b = (0.299 * pixel.0[0] as f64 + 0.587 * pixel.0[1] as f64 + 0.114 * pixel.0[2] as f64)
        .round() as u8;
    Rgba([b, b, b, pixel[3]])
}

fn anomylize(a: &Rgba<u8>, b: Rgba<u8>) -> Rgba<u8> {
    let c = 1.75;
    let d = 1.0 * c + 1.0;
    Rgba([
        ((c * b[0] as f64 + 1.0 * a[0] as f64) / d).round() as u8,
        ((c * b[1] as f64 + 1.0 * a[1] as f64) / d).round() as u8,
        ((c * b[2] as f64 + 1.0 * a[2] as f64) / d).round() as u8,
        a[3]
    ])
}

fn blindMK(a: &Rgba<u8>, b: &str) -> Rgba<u8> {
    #![allow(non_snake_case)]
    /*let a = Rgba([
        a[0] as f64,
        a[1] as f64,
        a[2] as f64,
        a[3] as f64,
    ]);*/
    let mut rBlind = HashMap::new();
    rBlind.insert(
        "protan",
        Info {
            cpu: 0.735,
            cpv: 0.265,
            am: 1.273463,
            ayi: -0.073894,
        },
    );
    rBlind.insert(
        "deutan",
        Info {
            cpu: 1.14,
            cpv: -0.14,
            am: 0.968437,
            ayi: 0.003331,
        },
    );
    rBlind.insert(
        "tritan",
        Info {
            cpu: 0.171,
            cpv: -0.003,
            am: 0.062921,
            ayi: 0.292119,
        },
    );
    /*let powGammaLookup: Vec<f64> = {
        let mut v = Vec::with_capacity(256);
        for a in 0..256 {
            v.push((a as f64 / 255.0).powf(2.2));
        }
        v
    };*/
    // replace this super inefficient lookup table
    let powGammaLookup = |n| (n as f64 / 255.0).powf(2.2);

    let d = 0.312713;
let  e = 0.329016;
let  f = 0.358271;
let  g = a[2];
let  h = a[1];
let  i = a[0];
let  j = powGammaLookup(i);
let  k = powGammaLookup(h);
let  l = powGammaLookup(g);
//let  m = 0.430574 * j + 0.34155 * k + 0.178325 * l;
let  n = 0.222015 * j + 0.706655 * k + 0.07133 * l;
//let  o = 0.020183 * j + 0.129553 * k + 0.93918 * l;
//let  p = m + n + o;
let  q = 0.0;
let  r = 0.0;

 let s = d * n / e;
 let t = f * n / e;
 let v = 0.0;

let  u = if q < rBlind[b].cpu { (rBlind[b].cpv - r) / (rBlind[b].cpu - q) }
else { (r - rBlind[b].cpv) / (q - rBlind[b].cpu) };

let w = r - q * u;
let  x = (rBlind[b].ayi - w) / (u - rBlind[b].am);
let  y = u * x + w;
let  z = x * n / y;
let  A = n;
let  B = (1.0 - (x + y)) * n / y;
let mut C = 3.063218 * z - 1.393325 * A - 0.475802 * B;
let mut D = - 0.969243 * z + 1.875966 * A + 0.041555 * B;
let mut E = 0.067871 * z - 0.228834 * A + 1.069251 * B;
let  F = s - z;
let  G = t - B;

let  dr = 3.063218 * F - 1.393325 * v - 0.475802 * G;
let  dg = - 0.969243 * F + 1.875966 * v + 0.041555 * G;
let  db = 0.067871 * F - 0.228834 * v + 1.069251 * G;

let step0 = |dx, v| {
    if dx != 0.0 {
        ((if v < 0.0 { 0.0 } else { 1.0 }) - v) / dx
    } else { 0.0 }
};
//let  H = dr ? ((C < 0 ? 0 : 1) - C) / dr : 0;
let H = step0(dr, C);
//let  I = dg ? ((D < 0 ? 0 : 1) - D) / dg : 0;
let I = step0(dg, D);
//let  J = db ? ((E < 0 ? 0 : 1) - E) / db : 0;
let J = step0(db, E);

// K = Math.max(H > 1 || H < 0 ? 0 : H
//            , I > 1 || I < 0 ? 0 : I
//            , J > 1 || J < 0 ? 0 : J);
let catch_outlier = |n| {
    if n > 1.0 || n < 0.0 { 0.0 } else { n }
};
let  K = catch_outlier(H)
        .max(catch_outlier(I))
        .max(catch_outlier(J));

  C += K * dr;
  D += K * dg;
  E += K * db;

    Rgba([
        inverse_pow(C),
        inverse_pow(D),
        inverse_pow(E),
        a[3],
    ])
}

struct Info {
    cpu: f64,
    cpv: f64,
    am:  f64,
    ayi: f64,
}

fn inverse_pow(a: f64) -> u8 {
   (255.0 *
    if a <= 0.0 { 0.0 } else {
        if a >= 1.0 { 1.0 } else { a.powf( 1.0 / 2.2) }
    }
    ) as u8
}
