use std::{str::FromStr, time::Duration};

use clap::Parser;
use common::cache;
use common::ipc::{self, Answer, BgInfo, IpcSocket, RequestSend};
use common::mmap::Mmap;

mod imgproc;
use imgproc::*;

mod cli;
use cli::{Awww, CliImage, CropGravity, Filter, ResizeStrategy};

fn main() -> Result<(), String> {
    common::log::init(common::log::Filter::Trace);
    let awww = Awww::parse();

    let all = match &awww {
        Awww::Clear(clear) => clear.all,
        Awww::Restore(restore) => restore.all,
        Awww::ClearCache => {
            return cache::clean().map_err(|e| format!("failed to clean the cache: {e}"));
        }
        Awww::Img(img) => img.all,
        Awww::Toggle(toggle) => toggle.all,
        Awww::Pause(pause) => pause.all,
        Awww::Unpause(unpause) => unpause.all,
        Awww::Kill(kill) => kill.all,
        Awww::Query(query) => query.all,
    };

    let namespaces = if all {
        IpcSocket::all_namespaces().map_err(|e| e.to_string())?
    } else {
        match &awww {
            Awww::Clear(clear) => clear.namespace.clone(),
            Awww::Restore(restore) => restore.namespace.clone(),
            Awww::ClearCache => {
                return cache::clean().map_err(|e| format!("failed to clean the cache: {e}"));
            }
            Awww::Img(img) => img.namespace.clone(),
            Awww::Toggle(toggle) => toggle.namespace.clone(),
            Awww::Pause(pause) => pause.namespace.clone(),
            Awww::Unpause(unpause) => unpause.namespace.clone(),
            Awww::Kill(kill) => kill.namespace.clone(),
            Awww::Query(query) => query.namespace.clone(),
        }
    };

    let mut infos = Vec::new();
    for namespace in &namespaces {
        let socket = IpcSocket::client(namespace).map_err(|err| err.to_string())?;
        loop {
            RequestSend::Ping.send(&socket).map_err(|e| e.to_string())?;
            let bytes = socket.recv().map_err(|err| err.to_string())?;
            let answer = Answer::receive(bytes);
            if let Answer::Ping(configured) = answer {
                if configured {
                    break;
                }
            } else {
                return Err("Daemon did not return Answer::Ping, as expected".to_string());
            }
            std::thread::sleep(Duration::from_millis(1));
        }

        if let Some(info) = process_awww_args(&awww, namespace)? {
            infos.push(info);
        }
    }

    if !infos.is_empty() {
        if let Awww::Query(query) = awww
            && query.json
        {
            use jzon::{JsonValue, object, stringify_pretty};
            let mut buf = String::new();
            for (namespace, infos) in namespaces.iter().zip(infos) {
                let mut arr = JsonValue::new_array();
                for info in infos {
                    let displaying = match info.img {
                        ipc::BgImg::Color(color) => {
                            object! { color: format!("#{:x}", u32::from_ne_bytes(color)) }
                        }
                        ipc::BgImg::Img(img) => {
                            object! { image: img.as_ref() }
                        }
                    };
                    _ = arr.push(object! {
                        name: info.name.as_ref(),
                        width: info.dim.0,
                        height: info.dim.1,
                        scale: info.scale_factor.to_f32(),
                        displaying: displaying
                    });
                }
                buf = format!("{buf}\n\"{namespace}\": {},", stringify_pretty(arr, 4));
            }
            buf.pop(); // delete trailing comma
            println!("{{{buf}\n}}");
        } else {
            for (namespace, infos) in namespaces.iter().zip(infos) {
                for info in infos {
                    println!("{namespace}: {info}");
                }
            }
        }
    }
    Ok(())
}

fn process_awww_args(args: &Awww, namespace: &str) -> Result<Option<Box<[BgInfo]>>, String> {
    let request = match make_request(args, namespace)? {
        Some(request) => request,
        None => return Ok(None),
    };
    let socket = IpcSocket::client(namespace).map_err(|err| err.to_string())?;
    request.send(&socket).map_err(|e| e.to_string())?;
    let bytes = socket.recv().map_err(|err| err.to_string())?;
    drop(socket);
    match Answer::receive(bytes) {
        Answer::Info(infos) => {
            if let Awww::Query(_) = args {
                return Ok(Some(infos));
            }
        }
        Answer::Ok => {
            if let Awww::Kill(_) = args {
                #[cfg(debug_assertions)]
                let tries = 20;
                #[cfg(not(debug_assertions))]
                let tries = 10;
                let path = IpcSocket::path(namespace);
                for _ in 0..tries {
                    if rustix::fs::access(&path, rustix::fs::Access::EXISTS).is_err() {
                        return Ok(None);
                    }
                    std::thread::sleep(Duration::from_millis(100));
                }
                return Err(format!(
                    "Could not confirm socket deletion at: {}",
                    path.display()
                ));
            }
        }
        Answer::Ping(_) => {
            return Ok(None);
        }
    }
    Ok(None)
}

fn make_request(args: &Awww, namespace: &str) -> Result<Option<RequestSend>, String> {
    match args {
        Awww::Clear(c) => {
            let (format, _, _) = get_format_dims_and_outputs(&[], namespace)?;
            let mut color = c.color;
            if format.must_swap_r_and_b_channels() {
                color.swap(0, 2);
            }
            let clear = ipc::ClearSend {
                color,
                outputs: split_cmdline_outputs(&c.outputs),
            };
            Ok(Some(RequestSend::Clear(
                clear.create_request().map_err(|e| e.to_string())?,
            )))
        }
        Awww::Restore(restore) => {
            let requested_outputs = split_cmdline_outputs(&restore.outputs);
            restore_from_cache(&requested_outputs, namespace)?;
            Ok(None)
        }
        Awww::ClearCache => unreachable!("there is no request for clear-cache"),
        Awww::Img(img) => {
            let requested_outputs = split_cmdline_outputs(&img.outputs);
            let (format, dims, outputs) =
                get_format_dims_and_outputs(&requested_outputs, namespace)?;
            // let imgbuf = ImgBuf::new(&img.path)?;

            let img_request = make_img_request(
                img,
                namespace,
                &dims,
                format,
                &outputs,
                img.outputs.is_empty(),
            )?;

            Ok(Some(RequestSend::Img(img_request)))
        }
        Awww::Toggle(_) => Ok(Some(RequestSend::Toggle)),
        Awww::Pause(_) => Ok(Some(RequestSend::Pause)),
        Awww::Unpause(_) => Ok(Some(RequestSend::Unpause)),
        Awww::Kill(_) => Ok(Some(RequestSend::Kill)),
        Awww::Query(_) => Ok(Some(RequestSend::Query)),
    }
}

fn make_img_request(
    img: &cli::Img,
    namespace: &str,
    dims: &[(u32, u32)],
    pixel_format: ipc::PixelFormat,
    outputs: &[Vec<String>],
    update_cached_disconnected_outputs: bool,
) -> Result<Mmap, String> {
    let transition = make_transition(img);
    let mut img_req_builder = ipc::ImageRequestBuilder::new(transition)
        .map_err(|e| format!("failed to create ImageRequestBuilder: {e}"))?;

    let filter = img.filter.as_str();
    let resize = img.resize.as_str();
    let crop_gravity_str = img.crop_gravity.map(|v| v.as_str());

    let cache_path;

    match &img.image {
        CliImage::Color(color) => {
            let color_path = format!(
                "0x{:02x}{:02x}{:02x}{:02x}",
                color[0], color[1], color[2], color[3]
            );

            cache_path = color_path.clone();

            for (&dim, outputs) in dims.iter().zip(outputs) {
                img_req_builder.push(
                    ipc::ImgSend {
                        img: image::RgbaImage::from_pixel(dim.0, dim.1, image::Rgba(*color))
                            .to_vec()
                            .into_boxed_slice(),
                        path: color_path.clone(),
                        dim,
                        format: pixel_format,
                    },
                    namespace,
                    resize,
                    crop_gravity_str,
                    filter,
                    outputs,
                    None,
                );
            }
        }
        CliImage::Path(img_path) => {
            #[cfg(feature = "jxl")]
            jxl_oxide::integration::register_image_decoding_hook();
            let imgbuf = ImgBuf::new(img_path)?;
            match imgbuf.decode_prepare() {
                DecodeBuffer::RasterImage(imgbuf) => {
                    let img_raw = imgbuf.decode(pixel_format)?;

                    let path = match img_path.canonicalize() {
                        Ok(p) => p.display().to_string(),
                        Err(e) => {
                            if let Some("-") = img_path.to_str() {
                                "STDIN".to_string()
                            } else {
                                return Err(format!("failed no canonicalize image path: {e}"));
                            }
                        }
                    };

                    cache_path = path.clone();

                    for (&dim, outputs) in dims.iter().zip(outputs) {
                        let animation = if imgbuf.is_animated() {
                            match cache::load_animation_frames(
                                &path.clone(),
                                dim,
                                resize,
                                pixel_format,
                            ) {
                                Ok(Some(animation)) => Some(animation),
                                otherwise => {
                                    if let Err(e) = otherwise {
                                        eprintln!(
                                            "Error loading cache for {}: {e}",
                                            img_path.display()
                                        );
                                    }
                                    Some({
                                        ipc::Animation {
                                            animation: compress_frames(
                                                imgbuf.as_frames()?,
                                                dim,
                                                pixel_format,
                                                make_filter(img.filter),
                                                img.resize,
                                                img.fill_color,
                                            )?
                                            .into_boxed_slice(),
                                        }
                                    })
                                }
                            }
                        } else {
                            None
                        };

                        let img = match img.resize {
                            ResizeStrategy::No => img_pad(&img_raw, dim, img.fill_color),
                            ResizeStrategy::Crop => img_resize_crop(
                                &img_raw,
                                dim,
                                make_filter(img.filter),
                                img.crop_gravity,
                            )?,
                            ResizeStrategy::Fit => img_resize_fit(
                                &img_raw,
                                dim,
                                make_filter(img.filter),
                                img.fill_color,
                            )?,
                            ResizeStrategy::Stretch => {
                                img_resize_stretch(&img_raw, dim, make_filter(img.filter))?
                            }
                        };

                        img_req_builder.push(
                            ipc::ImgSend {
                                img,
                                path: path.clone(),
                                dim,
                                format: pixel_format,
                            },
                            namespace,
                            resize,
                            crop_gravity_str,
                            filter,
                            outputs,
                            animation,
                        );
                    }
                }
                // Vector images are different because we can render them at any scale. So we
                // always make sure to render them at the largest possible scale without distortion
                DecodeBuffer::VectorImage(imgbuf) => {
                    let path = match img_path.canonicalize() {
                        Ok(p) => p.display().to_string(),
                        Err(e) => {
                            if let Some("-") = img_path.to_str() {
                                "STDIN".to_string()
                            } else {
                                return Err(format!("failed no canonicalize image path: {e}"));
                            }
                        }
                    };

                    cache_path = path.clone();

                    for (&dim, outputs) in dims.iter().zip(outputs) {
                        let filter = img.filter.as_str();
                        let img_raw = imgbuf.decode(pixel_format, dim.0, dim.1)?;
                        let img = match img.resize {
                            ResizeStrategy::No => img_pad(&img_raw, dim, img.fill_color),
                            ResizeStrategy::Crop => img_resize_crop(
                                &img_raw,
                                dim,
                                make_filter(img.filter),
                                img.crop_gravity,
                            )?,
                            ResizeStrategy::Fit => img_resize_fit(
                                &img_raw,
                                dim,
                                make_filter(img.filter),
                                img.fill_color,
                            )?,
                            ResizeStrategy::Stretch => {
                                img_resize_stretch(&img_raw, dim, make_filter(img.filter))?
                            }
                        };
                        img_req_builder.push(
                            ipc::ImgSend {
                                img,
                                path: path.clone(),
                                dim,
                                format: pixel_format,
                            },
                            namespace,
                            resize,
                            crop_gravity_str,
                            filter,
                            outputs,
                            None,
                        );
                    }
                }
            }
        }
    }

    if update_cached_disconnected_outputs {
        img_req_builder.update_disconnected_caches(cache_path, namespace, outputs);
    }

    Ok(img_req_builder.build())
}

#[allow(clippy::type_complexity)]
fn get_format_dims_and_outputs(
    requested_outputs: &[String],
    namespace: &str,
) -> Result<(ipc::PixelFormat, Vec<(u32, u32)>, Vec<Vec<String>>), String> {
    let mut outputs: Vec<Vec<String>> = Vec::new();
    let mut dims: Vec<(u32, u32)> = Vec::new();
    let mut imgs: Vec<ipc::BgImg> = Vec::new();

    let socket = IpcSocket::client(namespace).map_err(|err| err.to_string())?;
    RequestSend::Query
        .send(&socket)
        .map_err(|e| e.to_string())?;
    let bytes = socket.recv().map_err(|err| err.to_string())?;
    drop(socket);
    let answer = Answer::receive(bytes);
    match answer {
        Answer::Info(infos) => {
            let mut format = ipc::PixelFormat::Argb;
            for info in &infos {
                format = info.pixel_format;
                let info_img = &info.img;
                let name = info.name.to_string();
                if !requested_outputs.is_empty() && !requested_outputs.contains(&name) {
                    continue;
                }
                let real_dim = info.real_dim();
                if let Some((_, output)) = dims
                    .iter_mut()
                    .zip(&imgs)
                    .zip(&mut outputs)
                    .find(|((dim, img), _)| real_dim == **dim && info_img == *img)
                {
                    output.push(name);
                } else {
                    outputs.push(vec![name]);
                    dims.push(real_dim);
                    imgs.push(info_img.clone());
                }
            }
            if outputs.is_empty() {
                Err("none of the requested outputs are valid".to_owned())
            } else {
                Ok((format, dims, outputs))
            }
        }
        _ => unreachable!(),
    }
}

fn split_cmdline_outputs(outputs: &str) -> Box<[String]> {
    outputs
        .split(',')
        .map(ToOwned::to_owned)
        .filter(|s| !s.is_empty())
        .collect()
}

fn restore_from_cache(requested_outputs: &[String], namespace: &str) -> Result<(), String> {
    let (_, _, outputs) = get_format_dims_and_outputs(requested_outputs, namespace)?;

    for output in outputs.iter().flatten() {
        if let Err(e) = restore_output(output, namespace) {
            eprintln!("WARNING: failed to load cache for output {output}: {e}");
        }
    }

    Ok(())
}

fn restore_output(output: &str, namespace: &str) -> Result<(), String> {
    let cache_data = common::cache::read_cache_file(output)
        .map_err(|e| format!("failed to read cache file: {e}"))?;
    let cache = match common::cache::get_previous_image_cache(output, namespace, &cache_data) {
        Ok(Some(cache)) => cache,
        Ok(None) => return Err("cache entry does not exist".to_string()),
        Err(e) => return Err(e.to_string()),
    };

    let crop_gravity = cache
        .crop_gravity
        .map(|v| CropGravity::from_str(v).unwrap_or_default());

    process_awww_args(
        &Awww::Img(cli::Img {
            all: false,
            image: cli::parse_image(cache.img_path)?,
            outputs: output.to_string(),
            namespace: vec![namespace.to_string()],
            #[allow(deprecated)]
            no_resize: false,
            resize: ResizeStrategy::from_str(cache.resize).unwrap_or(ResizeStrategy::Crop),
            crop_gravity,
            fill_color: [0, 0, 0, 255],
            filter: Filter::from_str(cache.filter).unwrap_or(Filter::Lanczos3),
            transition_type: cli::TransitionType::None,
            transition_step: std::num::NonZeroU8::MAX,
            transition_duration: 0.0,
            transition_fps: 30,
            transition_angle: 0.0,
            transition_pos: cli::CliPosition {
                x: cli::CliCoord::Pixel(0.0),
                y: cli::CliCoord::Pixel(0.0),
            },
            invert_y: false,
            transition_bezier: (0.0, 0.0, 0.0, 0.0),
            transition_wave: (0.0, 0.0),
        }),
        namespace,
    )?;
    Ok(())
}
