use std::fs::File;
use std::fs;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use crate::types::Result;
use crate::utils::log::{success};
use image::{DynamicImage, ImageFormat, ColorType};
use image::ImageEncoder;
use image::codecs::jpeg::JpegEncoder;
use image::codecs::png::{PngEncoder, CompressionType, FilterType};
use image::codecs::webp::WebPEncoder;
use super::commands::{CompressArgs, ImgFormat, PngLevel};

fn detect_input_format(input: &Path) -> Option<ImgFormat> {
    if let Ok(fmt) = ImageFormat::from_path(input) {
        match fmt {
            ImageFormat::Png => Some(ImgFormat::Png),
            ImageFormat::Jpeg => Some(ImgFormat::Jpeg),
            ImageFormat::WebP => Some(ImgFormat::Webp),
            ImageFormat::Gif => Some(ImgFormat::Gif),
            ImageFormat::Tiff => Some(ImgFormat::Tiff),
            _ => None,
        }
    } else { None }
}

fn infer_out_dir(input: &Path, out_dir: Option<PathBuf>) -> PathBuf {
    match out_dir {
        Some(d) => d,
        None => input.parent().unwrap_or_else(|| Path::new(".")).to_path_buf(),
    }
}

fn build_out_path(input: &Path, out_dir: Option<PathBuf>, out_name: Option<String>, fmt: &ImgFormat) -> PathBuf {
    let dir = infer_out_dir(input, out_dir);
    let stem_default = input.file_stem().unwrap_or_default().to_string_lossy().to_string();
    let stem = out_name.unwrap_or_else(|| format!("{}.compress", stem_default));
    let ext = match fmt {
        ImgFormat::Png => "png",
        ImgFormat::Jpeg => "jpg",
        ImgFormat::Webp => "webp",
        ImgFormat::Gif => "gif",
        ImgFormat::Tiff => "tiff",
        ImgFormat::Avif => "avif",
        ImgFormat::Bmp => "bmp",
        ImgFormat::Dds => "dds",
        ImgFormat::Exr => "exr",
        ImgFormat::Ff => "ff",
        ImgFormat::Hdr => "hdr",
        ImgFormat::Ico => "ico",
        ImgFormat::Pnm => "pnm",
        ImgFormat::Qoi => "qoi",
        ImgFormat::Tga => "tga",
    };
    dir.join(format!("{}.{}", stem, ext))
}

pub fn execute(args: CompressArgs) -> Result<()> {
    let input = args.input;
    if !input.exists() { return Err(format!("input not found: {}", input.display()).into()); }

    let img = image::open(&input)?;
    let target_fmt = args.format.unwrap_or_else(|| detect_input_format(&input).unwrap_or(ImgFormat::Png));
    let out = build_out_path(&input, args.out_dir, args.out_name, &target_fmt);
    if out.exists() { return Err(format!("output already exists: {}", out.display()).into()); }
    if let Some(parent) = out.parent() { fs::create_dir_all(parent)?; }

    match target_fmt {
        ImgFormat::Jpeg => encode_jpeg(&img, &out, args.quality)?,
        ImgFormat::Png => encode_png(&img, &out, args.png_level)?,
        ImgFormat::Webp => encode_webp_lossless(&img, &out)?,
        ImgFormat::Dds => return Err("dds only supports decoding; cannot encode".into()),
        ImgFormat::Gif | ImgFormat::Tiff | ImgFormat::Avif | ImgFormat::Bmp | ImgFormat::Exr | ImgFormat::Ff | ImgFormat::Hdr | ImgFormat::Ico | ImgFormat::Pnm | ImgFormat::Qoi | ImgFormat::Tga => {
            img.save_with_format(&out, to_image_format(&target_fmt))?
        }
    }

    success(&format!("saved: {}", out.display()));
    Ok(())
}

fn to_image_format(fmt: &ImgFormat) -> ImageFormat {
    match fmt {
        ImgFormat::Png => ImageFormat::Png,
        ImgFormat::Jpeg => ImageFormat::Jpeg,
        ImgFormat::Webp => ImageFormat::WebP,
        ImgFormat::Gif => ImageFormat::Gif,
        ImgFormat::Tiff => ImageFormat::Tiff,
        ImgFormat::Avif => ImageFormat::Avif,
        ImgFormat::Bmp => ImageFormat::Bmp,
        ImgFormat::Dds => ImageFormat::Dds,
        ImgFormat::Exr => ImageFormat::OpenExr,
        ImgFormat::Ff => ImageFormat::Farbfeld,
        ImgFormat::Hdr => ImageFormat::Hdr,
        ImgFormat::Ico => ImageFormat::Ico,
        ImgFormat::Pnm => ImageFormat::Pnm,
        ImgFormat::Qoi => ImageFormat::Qoi,
        ImgFormat::Tga => ImageFormat::Tga,
    }
}

fn encode_jpeg(img: &DynamicImage, out: &Path, quality: u8) -> Result<()> {
    let file = File::create(out)?;
    let writer = BufWriter::new(file);
    let mut encoder = JpegEncoder::new_with_quality(writer, quality);
    let rgb = img.to_rgb8();
    encoder.write_image(&rgb, rgb.width(), rgb.height(), ColorType::Rgb8.into())?;
    Ok(())
}

fn encode_png(img: &DynamicImage, out: &Path, level: PngLevel) -> Result<()> {
    let file = File::create(out)?;
    let writer = BufWriter::new(file);
    let (compression, filter) = match level {
        PngLevel::Fast => (CompressionType::Fast, FilterType::NoFilter),
        PngLevel::Default => (CompressionType::Default, FilterType::Sub),
        PngLevel::Best => (CompressionType::Best, FilterType::Paeth),
    };
    let mut encoder = PngEncoder::new_with_quality(writer, compression, filter);
    let rgba = img.to_rgba8();
    encoder.write_image(&rgba, rgba.width(), rgba.height(), ColorType::Rgba8.into())?;
    Ok(())
}

fn encode_webp_lossless(img: &DynamicImage, out: &Path) -> Result<()> {
    let file = File::create(out)?;
    let writer = BufWriter::new(file);
    let mut encoder = WebPEncoder::new_lossless(writer);
    let rgba = img.to_rgba8();
    encoder.write_image(&rgba, rgba.width(), rgba.height(), ColorType::Rgba8.into())?;
    Ok(())
}
