use std::{
    fs::File, io::{BufReader, Cursor, Read, Seek, SeekFrom, Write}, path::Path
};
use byteorder::ReadBytesExt;

use crate::{
    common::{apply_palette, get_palette, rgba_to_indexed, CommonHeader, CzError, CzHeader, CzVersion, ExtendedHeader},
    formats::{cz0, cz1, cz2, cz3, cz4},
};

pub struct DynamicCz {
    header_common: CommonHeader,
    header_extended: Option<ExtendedHeader>,
    palette: Option<Vec<[u8; 4]>>,
    bitmap: Vec<u8>,
}

impl DynamicCz {
    pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<Self, CzError> {
        let mut img_file = BufReader::new(std::fs::File::open(path)?);

        Self::decode(&mut img_file)
    }

    pub fn save_as_png<P: ?Sized + AsRef<Path>>(&self, path: &P) -> Result<(), png::EncodingError> {
        let file = std::fs::File::create(path).unwrap();
        let writer = std::io::BufWriter::new(file);

        let mut encoder = png::Encoder::new(
            writer,
            self.header().width() as u32,
            self.header().height() as u32,
        );
        encoder.set_color(png::ColorType::Rgba);
        encoder.set_depth(png::BitDepth::Eight);
        let mut writer = encoder.write_header()?;

        writer.write_image_data(self.bitmap())?; // Save

        Ok(())
    }
}

impl DynamicCz {
    pub fn decode<T: Seek + ReadBytesExt + Read>(input: &mut T) -> Result<Self, CzError> {
        // Get the header common to all CZ images
        let header_common = CommonHeader::new(input)?;
        let mut header_extended = None;
        if header_common.length() > 15 && header_common.version() != CzVersion::CZ2 {
            header_extended = Some(ExtendedHeader::new(input, &header_common)?);
        }
        input.seek(SeekFrom::Start(header_common.length() as u64))?;

        // Get the color palette if the bit depth is 8 or less
        let palette = if header_common.depth() <= 8 {
            let color_count = 1 << header_common.depth();
            Some(get_palette(input, color_count)?)
        } else {
            None
        };

        // Get the image data as a bitmap
        let mut bitmap = match header_common.version() {
            CzVersion::CZ0 => cz0::decode(input)?,
            CzVersion::CZ1 => cz1::decode(input)?,
            CzVersion::CZ2 => cz2::decode(input)?,
            CzVersion::CZ3 => cz3::decode(input, &header_common)?,
            CzVersion::CZ4 => cz4::decode(input, &header_common)?,
            CzVersion::CZ5 => unimplemented!(),
        };

        let image_size = header_common.width() as usize * header_common.height() as usize;
        if bitmap.len() != image_size * (header_common.depth() >> 3) as usize {
            // If the bitmap is smaller or larger than the image size, it is likely wrong
            return Err(CzError::Corrupt)
        }

        if let Some(palette) = &palette {
            bitmap = apply_palette(&bitmap, palette)?;
        }

        Ok(Self {
            header_common,
            header_extended,
            palette,
            bitmap,
        })
    }

    pub fn save_as_cz<T: Into<std::path::PathBuf>>(&self, path: T) -> Result<(), CzError> {
        let mut out_file = File::create(path.into())?;

        self.header_common.write_into(&mut out_file)?;

        if let Some(ext) = self.header_extended {
            ext.write_into(&mut out_file)?;
        }

        let output_bitmap;
        match &self.palette {
            Some(pal) if self.header_common.depth() <= 8 => {
                output_bitmap = rgba_to_indexed(&self.bitmap(), &pal)?
            },
            _ => output_bitmap = self.bitmap().clone()
        }

        match self.header_common.version() {
            CzVersion::CZ0 => cz0::encode(&mut out_file, &self.bitmap)?,
            CzVersion::CZ1 => todo!(),
            CzVersion::CZ2 => todo!(),
            CzVersion::CZ3 => todo!(),
            CzVersion::CZ4 => todo!(),
            CzVersion::CZ5 => todo!(),
        }

        Ok(())
    }

    pub fn header(&self) -> &CommonHeader {
        &self.header_common
    }

    pub fn header_mut(&mut self) -> &mut CommonHeader {
        &mut self.header_common
    }

    pub fn set_header(&mut self, header: &CommonHeader) {
        self.header_common = header.to_owned()
    }

    pub fn bitmap(&self) -> &Vec<u8> {
        &self.bitmap
    }

    pub fn into_bitmap(self) -> Vec<u8> {
        self.bitmap
    }

    pub fn set_bitmap(&mut self, bitmap: Vec<u8>, width: u16, height: u16) -> Result<(), CzError> {
        if bitmap.len() != width as usize * height as usize {
            return Err(CzError::BitmapFormat)
        }

        self.bitmap = bitmap;

        self.header_mut().set_width(width);
        self.header_mut().set_height(height);

        Ok(())
    }
}