#![allow(non_upper_case_globals)]

extern crate getopts;
extern crate glob;
extern crate png;
extern crate term;

use std::io;
use std::io::prelude::*;
use std::path::Path;
use std::fs::File;
use std::env;

use getopts::{Matches, Options, ParsingStyle};
use term::{color, Attr};

fn parse_args() -> Option<Matches> {
    let args: Vec<String> = env::args().collect();
    let mut opts = Options::new();
    opts.optflag("c", "", "colorize output (for ANSI terminals)")
        .optflag("q", "", "test quietly (output only errors)")
        //.optflag("t", "", "print contents of tEXt chunks (can be used with -q)");
        .optflag("v", "", "test verbosely (print most chunk data)")
        .parsing_style(ParsingStyle::StopAtFirstFree);
    if args.len() > 1 {
        match opts.parse(&args[1..]) {
            Ok(matches) => return Some(matches),
            Err(err) => println!("{}", err)
        }
    }
    println!("{}", opts.usage(&format!("Usage: pngcheck [-cpt] [file ...]")));
    None
}

#[derive(Clone, Copy)]
struct Config {
    quiet: bool,
    verbose: bool,
    color: bool
}

fn display_interlaced(i: bool) -> &'static str {
    if i {
        "interlaced"
    } else {
        "non-interlaced"
    }
}

fn display_image_type(bits: u8, color: png::ColorType) -> String {
    use png::ColorType::*;
    format!(
        "{}-bit {}",
        bits,
        match color {
            Grayscale => "grayscale",
            RGB => "RGB",
            Indexed => "palette",
            GrayscaleAlpha => "grayscale+alpha",
            RGBA => "RGB+alpha"
        }
    )
}
// channels after expansion of tRNS
fn final_channels(c: png::ColorType, trns: bool) -> u8 {
    use png::ColorType::*;
    match c {
        Grayscale => 1 + if trns { 1 } else { 0 },
        RGB => 3,
        Indexed => 3 + if trns { 1 } else { 0 },
        GrayscaleAlpha => 2,
        RGBA => 4
    }
}
fn check_image<P: AsRef<Path>>(c: Config, fname: P) -> io::Result<()> {
    // TODO improve performance by resusing allocations from decoder
    use png::Decoded::*;
    let mut t = try!(term::stdout().ok_or(io::Error::new(
        io::ErrorKind::Other,
        "could not open terminal"
    )));
    let mut data = vec![0; 10*1024];
    let data_p = data.as_mut_ptr();
    let mut reader = io::BufReader::new(try!(File::open(&fname)));
    let fname = fname.as_ref().to_string_lossy();
    let n = try!(reader.read(&mut data));
    let mut buf = &data[..n];
    let mut pos = 0;
    let mut decoder = png::StreamingDecoder::new();
    // Image data
    let mut width = 0;
    let mut height = 0;
    let mut color = png::ColorType::Grayscale;
    let mut bits = 0;
    let mut trns = false;
    let mut interlaced = false;
    let mut compressed_size = 0;
    let mut n_chunks = 0;
    let mut have_idat = false;
    macro_rules! c_ratio(
        // TODO add palette entries to compressed_size
        () => ({
            compressed_size as f32/(
                height as u64 *
                (width as u64 * final_channels(color, trns) as u64 * bits as u64 + 7)>>3
            ) as f32
        });
    );
    let display_error = |err| -> Result<_, io::Error> {
        let mut t = try!(term::stdout().ok_or(io::Error::new(
            io::ErrorKind::Other,
            "could not open terminal"
        )));
        if c.verbose {
            if c.color {
                print!(": ");
                try!(t.fg(color::RED));
                try!(writeln!(t, "{}", err));
                try!(t.attr(Attr::Bold));
                try!(write!(t, "ERRORS DETECTED"));
                try!(t.reset());
            } else {
                println!(": {}", err);
                print!("ERRORS DETECTED")
            }
            println!(" in {}", fname);
        } else {
            if !c.quiet { if c.color {
                try!(t.fg(color::RED));
                try!(t.attr(Attr::Bold));
                try!(write!(t, "ERROR"));
                try!(t.reset());
                try!(write!(t, ": "));
                try!(t.fg(color::YELLOW));
                try!(writeln!(t, "{}", fname));
                try!(t.reset());
            } else {
                println!("ERROR: {}", fname)
            }}
            print!("{}: ", fname);
            if c.color {
                try!(t.fg(color::RED));
                try!(writeln!(t, "{}", err));
                try!(t.reset());
            } else {
                println!("{}", err);
            }
            
        }
        Ok(())
    };
    
    if c.verbose {
        print!("File: ");
        if c.color {
            try!(t.attr(Attr::Bold));
            try!(write!(t, "{}", fname));
            try!(t.reset());
        } else {
            print!("{}", fname);
        }
        print!(" ({}) bytes", data.len())
            
    }
    loop {
        if buf.len() == 0 {
            // circumvent borrow checker
            let n = try!(reader.read(unsafe{::std::mem::transmute(
                ::std::slice::from_raw_parts_mut(data_p, data.len())
            )}));
            buf = &data[..n];
        }
        match decoder.update(buf) {
            Ok((_, ImageEnd)) => {
                if !have_idat {
                    try!(display_error(png::DecodingError::Format("IDAT chunk missing".into())));
                    break;
                }
                if !c.verbose && !c.quiet {
                    if c.color {
                        try!(t.fg(color::GREEN));
                        try!(t.attr(Attr::Bold));
                        try!(write!(t, "OK"));
                        try!(t.reset());
                        try!(write!(t, ": "));
                        try!(t.fg(color::YELLOW));
                        try!(write!(t, "{}", fname));
                        try!(t.reset());
                    } else {
                        print!("OK: {}", fname)
                    }
                    println!(
                        " ({}x{}, {}{}, {}, {:.1}%)",
                        width,
                        height,
                        display_image_type(bits, color),
                        (if trns { "+trns" } else { "" }),
                        display_interlaced(interlaced),
                        100.0*(1.0-c_ratio!())
                    )
                } else if !c.quiet {
                    println!("");
                    if c.color {
                        try!(t.fg(color::GREEN));
                        try!(t.attr(Attr::Bold));
                        try!(write!(t, "No errors detected "));
                        try!(t.reset());
                    } else {
                        print!("No errors detected ");
                    }
                    println!(
                        "in {} ({} chunks, {:.1}% compression)",
                        fname,
                        n_chunks,
                        100.0*(1.0-c_ratio!())
                    )
                }
                break
            },
            Ok((n, res)) => {
                buf = &buf[n..];
                pos += n;
                match res {
                    Header(w, h, b, c, i) => {
                        width = w;
                        height = h;
                        bits = b as u8;
                        color = c;
                        interlaced = i;
                    }
                    ChunkBegin(len, type_str) => {
                        use png::chunk;
                        n_chunks += 1;
                        if c.verbose {
                            let chunk = String::from_utf8_lossy(&type_str);
                            println!("");
                            print!("  chunk ");
                            if c.color {
                                try!(t.fg(color::YELLOW));
                                try!(write!(t, "{}", chunk));
                                try!(t.reset());
                            } else {
                                print!("{}", chunk)
                            }
                            print!(
                                " at offset {:#07x}, length {}",
                                pos - 4, // substract chunk name length
                                len
                            )
                        }
                        match type_str {
                            chunk::IDAT => {
                                have_idat = true;
                                compressed_size += len
                            },
                            chunk::tRNS => {
                                trns = true;
                            },    
                            _ => ()
                        }
                    }
                    ImageData(_) => {
                        //println!("got {} bytes of image data", data.len())
                    }
                    ChunkComplete(_, type_str) if c.verbose => {
                        use png::chunk::*;
                        match type_str {
                            IHDR => {
                                println!("");
                                print!(
                                    "    {} x {} image, {}{}, {}",
                                    width,
                                    height,
                                    display_image_type(bits, color),
                                    (if trns { "+trns" } else { "" }),
                                    display_interlaced(interlaced),
                                );
                            }
                            _ => ()
                        }
                    }
                    AnimationControl(actl) => {
                        println!("");
                        print!(
                            "    {} frames, {} plays",
                            actl.num_frames,
                            actl.num_plays,
                        );
                    }
                    FrameControl(fctl) => {
                        println!("");
                        println!(
                            "    sequence #{}, {} x {} pixels @ ({}, {})",
                            fctl.sequence_number,
                            fctl.width,
                            fctl.height,
                            fctl.x_offset,
                            fctl.y_offset,
                            /*fctl.delay_num,
                            fctl.delay_den,
                            fctl.dispose_op,
                            fctl.blend_op,*/
                        );
                        print!(
                            "    {}/{} s delay, dispose: {}, blend: {}",
                            fctl.delay_num,
                            if fctl.delay_den == 0 { 100 } else {fctl.delay_den},
                            fctl.dispose_op,
                            fctl.blend_op,
                        );
                    }
                    _ => ()
                }
                //println!("{} {:?}", n, res)
            },
            Err(err) => {
                let _ = display_error(err);
                break
            }
        }
    }
    Ok(())
}

fn main() {
    if let Some(m) = parse_args() {
        let config = Config {
            quiet: m.opt_present("q"),
            verbose: m.opt_present("v"),
            color: m.opt_present("c")
        };
        for file in m.free {
            match if file.contains("*") {
                (|| -> io::Result<_> {
                    for entry in try!(glob::glob(&file).map_err(|err| {
                        io::Error::new(io::ErrorKind::Other, err.msg)
                    })) {
                        try!(check_image(config, try!(entry.map_err(|_| {
                            io::Error::new(io::ErrorKind::Other, "glob error")
                        }))))
                    }
                    Ok(())
                })()
            } else {
                check_image(config, &file)
            } {
                Ok(_) => (),
                Err(err) => {
                    println!("{}: {}", file, err);
                    break
                }
            }
            
        }
    }
}