use std::fmt;
use std::io;
use std::io::Write;
use super::AutoBreak;
use super::{ArbitraryHeader, ArbitraryTuplType, BitmapHeader, GraymapHeader, PixmapHeader};
use super::{HeaderRecord, PNMHeader, PNMSubtype, SampleEncoding};
use color::{num_components, ColorType};
use byteorder::{BigEndian, WriteBytesExt};
enum HeaderStrategy {
    Dynamic,
    Subtype(PNMSubtype),
    Chosen(PNMHeader),
}
#[derive(Clone, Copy)]
pub enum FlatSamples<'a> {
    U8(&'a [u8]),
    U16(&'a [u16]),
}
pub struct PNMEncoder<W: Write> {
    writer: W,
    header: HeaderStrategy,
}
struct CheckedImageBuffer<'a> {
    _image: FlatSamples<'a>,
    _width: u32,
    _height: u32,
    _color: ColorType,
}
struct UncheckedHeader<'a> {
    header: &'a PNMHeader,
}
struct CheckedDimensions<'a> {
    unchecked: UncheckedHeader<'a>,
    width: u32,
    height: u32,
}
struct CheckedHeaderColor<'a> {
    dimensions: CheckedDimensions<'a>,
    color: ColorType,
}
struct CheckedHeader<'a> {
    color: CheckedHeaderColor<'a>,
    encoding: TupleEncoding<'a>,
    _image: CheckedImageBuffer<'a>,
}
enum TupleEncoding<'a> {
    PbmBits {
        samples: FlatSamples<'a>,
        width: u32,
    },
    Ascii {
        samples: FlatSamples<'a>,
    },
    Bytes {
        samples: FlatSamples<'a>,
    },
}
impl<W: Write> PNMEncoder<W> {
    
    
    
    
    
    pub fn new(writer: W) -> Self {
        PNMEncoder {
            writer,
            header: HeaderStrategy::Dynamic,
        }
    }
    
    
    
    
    
    
    
    pub fn with_subtype(self, subtype: PNMSubtype) -> Self {
        PNMEncoder {
            writer: self.writer,
            header: HeaderStrategy::Subtype(subtype),
        }
    }
    
    
    
    
    
    
    
    
    
    pub fn with_header(self, header: PNMHeader) -> Self {
        PNMEncoder {
            writer: self.writer,
            header: HeaderStrategy::Chosen(header),
        }
    }
    
    
    
    
    
    
    
    pub fn with_dynamic_header(self) -> Self {
        PNMEncoder {
            writer: self.writer,
            header: HeaderStrategy::Dynamic,
        }
    }
    
    
    
    
    pub fn encode<'s, S>(
        &mut self,
        image: S,
        width: u32,
        height: u32,
        color: ColorType,
    ) -> io::Result<()>
    where
        S: Into<FlatSamples<'s>>,
    {
        let image = image.into();
        match self.header {
            HeaderStrategy::Dynamic => self.write_dynamic_header(image, width, height, color),
            HeaderStrategy::Subtype(subtype) => {
                self.write_subtyped_header(subtype, image, width, height, color)
            }
            HeaderStrategy::Chosen(ref header) => {
                Self::write_with_header(&mut self.writer, header, image, width, height, color)
            }
        }
    }
    
    
    
    fn write_dynamic_header(
        &mut self,
        image: FlatSamples,
        width: u32,
        height: u32,
        color: ColorType,
    ) -> io::Result<()> {
        let depth = num_components(color) as u32;
        let (maxval, tupltype) = match color {
            ColorType::Gray(1) => (1, ArbitraryTuplType::BlackAndWhite),
            ColorType::GrayA(1) => (1, ArbitraryTuplType::BlackAndWhiteAlpha),
            ColorType::Gray(n @ 1...16) => ((1 << n) - 1, ArbitraryTuplType::Grayscale),
            ColorType::GrayA(n @ 1...16) => ((1 << n) - 1, ArbitraryTuplType::GrayscaleAlpha),
            ColorType::RGB(n @ 1...16) => ((1 << n) - 1, ArbitraryTuplType::RGB),
            ColorType::RGBA(n @ 1...16) => ((1 << n) - 1, ArbitraryTuplType::RGBAlpha),
            _ => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    &format!("Encoding colour type {:?} is not supported", color)[..],
                ))
            }
        };
        let header = PNMHeader {
            decoded: HeaderRecord::Arbitrary(ArbitraryHeader {
                width,
                height,
                depth,
                maxval,
                tupltype: Some(tupltype),
            }),
            encoded: None,
        };
        Self::write_with_header(&mut self.writer, &header, image, width, height, color)
    }
    
    fn write_subtyped_header(
        &mut self,
        subtype: PNMSubtype,
        image: FlatSamples,
        width: u32,
        height: u32,
        color: ColorType,
    ) -> io::Result<()> {
        let header = match (subtype, color) {
            (PNMSubtype::ArbitraryMap, color) => {
                return self.write_dynamic_header(image, width, height, color)
            }
            (PNMSubtype::Pixmap(encoding), ColorType::RGB(8)) => PNMHeader {
                decoded: HeaderRecord::Pixmap(PixmapHeader {
                    encoding,
                    width,
                    height,
                    maxval: 255,
                }),
                encoded: None,
            },
            (PNMSubtype::Graymap(encoding), ColorType::Gray(8)) => PNMHeader {
                decoded: HeaderRecord::Graymap(GraymapHeader {
                    encoding,
                    width,
                    height,
                    maxwhite: 255,
                }),
                encoded: None,
            },
            (PNMSubtype::Bitmap(encoding), ColorType::Gray(8))
            | (PNMSubtype::Bitmap(encoding), ColorType::Gray(1)) => PNMHeader {
                decoded: HeaderRecord::Bitmap(BitmapHeader {
                    encoding,
                    width,
                    height,
                }),
                encoded: None,
            },
            (_, _) => {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "Color type can not be represented in the chosen format",
                ))
            }
        };
        Self::write_with_header(&mut self.writer, &header, image, width, height, color)
    }
    
    
    
    fn write_with_header(
        writer: &mut Write,
        header: &PNMHeader,
        image: FlatSamples,
        width: u32,
        height: u32,
        color: ColorType,
    ) -> io::Result<()> {
        let unchecked = UncheckedHeader { header };
        unchecked
            .check_header_dimensions(width, height)?
            .check_header_color(color)?
            .check_sample_values(image)?
            .write_header(writer)?
            .write_image(writer)
    }
}
impl<'a> CheckedImageBuffer<'a> {
    fn check(
        image: FlatSamples<'a>,
        width: u32,
        height: u32,
        color: ColorType,
    ) -> io::Result<CheckedImageBuffer<'a>> {
        let components = num_components(color);
        let uwidth = width as usize;
        let uheight = height as usize;
        match Some(components)
            .and_then(|v| v.checked_mul(uwidth))
            .and_then(|v| v.checked_mul(uheight))
        {
            None => Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                &format!(
                    "Image dimensions invalid: {}×{}×{} (w×h×d)",
                    width, height, components
                )[..],
            )),
            Some(v) if v == image.len() => Ok(CheckedImageBuffer {
                _image: image,
                _width: width,
                _height: height,
                _color: color,
            }),
            Some(_) => Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                &"Image buffer does not correspond to size and colour".to_string()[..],
            )),
        }
    }
}
impl<'a> UncheckedHeader<'a> {
    fn check_header_dimensions(self, width: u32, height: u32) -> io::Result<CheckedDimensions<'a>> {
        if self.header.width() != width || self.header.height() != height {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Chosen header does not match Image dimensions",
            ));
        }
        Ok(CheckedDimensions {
            unchecked: self,
            width,
            height,
        })
    }
}
impl<'a> CheckedDimensions<'a> {
    
    
    
    fn check_header_color(self, color: ColorType) -> io::Result<CheckedHeaderColor<'a>> {
        let components = match color {
            ColorType::Gray(_) => 1,
            ColorType::GrayA(_) => 2,
            ColorType::Palette(_) | ColorType::RGB(_) | ColorType::BGR(_)=> 3,
            ColorType::RGBA(_) | ColorType::BGRA(_) => 4,
        };
        match *self.unchecked.header {
            PNMHeader {
                decoded: HeaderRecord::Bitmap(_),
                ..
            } => match color {
                ColorType::Gray(_) => (),
                _ => {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        "PBM format only support ColorType::Gray",
                    ))
                }
            },
            PNMHeader {
                decoded: HeaderRecord::Graymap(_),
                ..
            } => match color {
                ColorType::Gray(_) => (),
                _ => {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        "PGM format only support ColorType::Gray",
                    ))
                }
            },
            PNMHeader {
                decoded: HeaderRecord::Pixmap(_),
                ..
            } => match color {
                ColorType::RGB(_) => (),
                _ => {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        "PPM format only support ColorType::RGB",
                    ))
                }
            },
            PNMHeader {
                decoded:
                    HeaderRecord::Arbitrary(ArbitraryHeader {
                        depth,
                        ref tupltype,
                        ..
                    }),
                ..
            } => match (tupltype, color) {
                (&Some(ArbitraryTuplType::BlackAndWhite), ColorType::Gray(_)) => (),
                (&Some(ArbitraryTuplType::BlackAndWhiteAlpha), ColorType::GrayA(_)) => (),
                (&Some(ArbitraryTuplType::Grayscale), ColorType::Gray(_)) => (),
                (&Some(ArbitraryTuplType::GrayscaleAlpha), ColorType::GrayA(_)) => (),
                (&Some(ArbitraryTuplType::RGB), ColorType::RGB(_)) => (),
                (&Some(ArbitraryTuplType::RGBAlpha), ColorType::RGBA(_)) => (),
                (&None, _) if depth == components => (),
                (&Some(ArbitraryTuplType::Custom(_)), _) if depth == components => (),
                _ if depth != components => {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        format!("Depth mismatch: header {} vs. color {}", depth, components),
                    ))
                }
                _ => {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        "Invalid color type for selected PAM color type",
                    ))
                }
            },
        }
        Ok(CheckedHeaderColor {
            dimensions: self,
            color,
        })
    }
}
impl<'a> CheckedHeaderColor<'a> {
    fn check_sample_values(self, image: FlatSamples<'a>) -> io::Result<CheckedHeader<'a>> {
        let header_maxval = match self.dimensions.unchecked.header.decoded {
            HeaderRecord::Bitmap(_) => 1,
            HeaderRecord::Graymap(GraymapHeader { maxwhite, .. }) => maxwhite,
            HeaderRecord::Pixmap(PixmapHeader { maxval, .. }) => maxval,
            HeaderRecord::Arbitrary(ArbitraryHeader { maxval, .. }) => maxval,
        };
        
        let max_sample = match self.color {
            
            ColorType::Gray(n)
            | ColorType::GrayA(n)
            | ColorType::Palette(n)
            | ColorType::RGB(n)
            | ColorType::RGBA(n)
            | ColorType::BGR(n)
            | ColorType::BGRA(n) if n > 16 =>
            {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "Encoding colors with a bit depth greater 16 not supported",
                ))
            }
            ColorType::Gray(n)
            | ColorType::GrayA(n)
            | ColorType::Palette(n)
            | ColorType::RGB(n)
            | ColorType::RGBA(n)
            | ColorType::BGR(n)
            | ColorType::BGRA(n) => (1 << n) - 1,
        };
        
        if header_maxval < max_sample && !image.all_smaller(header_maxval) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "Sample value greater than allowed for chosen header",
            ));
        }
        let encoding = image.encoding_for(&self.dimensions.unchecked.header.decoded);
        let image = CheckedImageBuffer::check(
            image,
            self.dimensions.width,
            self.dimensions.height,
            self.color,
        )?;
        Ok(CheckedHeader {
            color: self,
            encoding,
            _image: image,
        })
    }
}
impl<'a> CheckedHeader<'a> {
    fn write_header(self, writer: &mut Write) -> io::Result<TupleEncoding<'a>> {
        self.header().write(writer)?;
        Ok(self.encoding)
    }
    fn header(&self) -> &PNMHeader {
        self.color.dimensions.unchecked.header
    }
}
struct SampleWriter<'a>(&'a mut Write);
impl<'a> SampleWriter<'a> {
    fn write_samples_ascii<V>(self, samples: V) -> io::Result<()>
    where
        V: Iterator,
        V::Item: fmt::Display,
    {
        let mut auto_break_writer = AutoBreak::new(self.0, 70);
        for value in samples {
            write!(auto_break_writer, "{} ", value)?;
        }
        auto_break_writer.flush()
    }
    fn write_pbm_bits<V>(self, samples: &[V], width: u32) -> io::Result<()>
    
    where
        V: Default + Eq + Copy,
    {
        
        let line_width = (width - 1) / 8 + 1;
        
        let mut line_buffer = Vec::with_capacity(line_width as usize);
        for line in samples.chunks(width as usize) {
            for byte_bits in line.chunks(8) {
                let mut byte = 0u8;
                for i in 0..8 {
                    
                    if let Some(&v) = byte_bits.get(i) {
                        if v == V::default() {
                            byte |= 1u8 << (7 - i)
                        }
                    }
                }
                line_buffer.push(byte)
            }
            self.0.write_all(line_buffer.as_slice())?;
            line_buffer.clear();
        }
        self.0.flush()
    }
}
impl<'a> FlatSamples<'a> {
    fn len(&self) -> usize {
        match *self {
            FlatSamples::U8(arr) => arr.len(),
            FlatSamples::U16(arr) => arr.len(),
        }
    }
    fn all_smaller(&self, max_val: u32) -> bool {
        match *self {
            FlatSamples::U8(arr) => arr.iter().any(|&val| u32::from(val) > max_val),
            FlatSamples::U16(arr) => arr.iter().any(|&val| u32::from(val) > max_val),
        }
    }
    fn encoding_for(&self, header: &HeaderRecord) -> TupleEncoding<'a> {
        match *header {
            HeaderRecord::Bitmap(BitmapHeader {
                encoding: SampleEncoding::Binary,
                width,
                ..
            }) => TupleEncoding::PbmBits {
                samples: *self,
                width,
            },
            HeaderRecord::Bitmap(BitmapHeader {
                encoding: SampleEncoding::Ascii,
                ..
            }) => TupleEncoding::Ascii { samples: *self },
            HeaderRecord::Arbitrary(_) => TupleEncoding::Bytes { samples: *self },
            HeaderRecord::Graymap(GraymapHeader {
                encoding: SampleEncoding::Ascii,
                ..
            })
            | HeaderRecord::Pixmap(PixmapHeader {
                encoding: SampleEncoding::Ascii,
                ..
            }) => TupleEncoding::Ascii { samples: *self },
            HeaderRecord::Graymap(GraymapHeader {
                encoding: SampleEncoding::Binary,
                ..
            })
            | HeaderRecord::Pixmap(PixmapHeader {
                encoding: SampleEncoding::Binary,
                ..
            }) => TupleEncoding::Bytes { samples: *self },
        }
    }
}
impl<'a> From<&'a [u8]> for FlatSamples<'a> {
    fn from(samples: &'a [u8]) -> Self {
        FlatSamples::U8(samples)
    }
}
impl<'a> From<&'a [u16]> for FlatSamples<'a> {
    fn from(samples: &'a [u16]) -> Self {
        FlatSamples::U16(samples)
    }
}
impl<'a> TupleEncoding<'a> {
    fn write_image(&self, writer: &mut Write) -> io::Result<()> {
        match *self {
            TupleEncoding::PbmBits {
                samples: FlatSamples::U8(samples),
                width,
            } => SampleWriter(writer).write_pbm_bits(samples, width),
            TupleEncoding::PbmBits {
                samples: FlatSamples::U16(samples),
                width,
            } => SampleWriter(writer).write_pbm_bits(samples, width),
            TupleEncoding::Bytes {
                samples: FlatSamples::U8(samples),
            } => writer.write_all(samples),
            TupleEncoding::Bytes {
                samples: FlatSamples::U16(samples),
            } => samples
                .iter()
                .map(|&sample| writer.write_u16::<BigEndian>(sample))
                .collect(),
            TupleEncoding::Ascii {
                samples: FlatSamples::U8(samples),
            } => SampleWriter(writer).write_samples_ascii(samples.iter()),
            TupleEncoding::Ascii {
                samples: FlatSamples::U16(samples),
            } => SampleWriter(writer).write_samples_ascii(samples.iter()),
        }
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn custom_header_and_color() {
        let data: [u8; 12] = [0, 0, 0, 1, 1, 1, 255, 255, 255, 0, 0, 0];
        let header = ArbitraryHeader {
            width: 2,
            height: 2,
            depth: 3,
            maxval: 255,
            tupltype: Some(ArbitraryTuplType::Custom("Palette".to_string())),
        };
        let mut output = Vec::new();
        PNMEncoder::new(&mut output)
            .with_header(header.into())
            .encode(&data[..], 2, 2, ColorType::Palette(8))
            .expect("Failed encoding custom color value");
    }
}