// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    time::Instant,

    os::fd::{
        AsFd as _,
        BorrowedFd
    },

    io::{
        Write,
        stdout,
        Stdin
    }
};

use base64::{
    Engine,
    engine
};

use image::{
    DynamicImage,
    imageops::FilterType,
    RgbaImage,
    FlatSamples
};

use rustix::{
    io::read,

    event::{
        PollFd,
        PollFlags,
        Timespec,
        poll
    },

    termios::{
        OptionalActions,
        Termios,
        Winsize,
        tcgetwinsize,
        tcsetattr
    }
};

use anyhow::{
    Context as _,
    Result
};

use super::ImageBackend;

use crate::image::{
    terminal::enable_raw_mode,
    config::DisplayConfig
};

pub(super) struct KittyBackend;

impl KittyBackend {
    pub(super) fn supported() -> Result<bool> {
        let stdin: Stdin = std::io::stdin();

        // save terminal attributes and disable canonical input processing mode
        let old_attributes: Termios = enable_raw_mode(&stdin)?;

        // generate red rgba test image
        let mut test_image: Vec<u8> = Vec::<u8>::with_capacity(32 * 32 * 4);
        (&mut test_image).extend(std::iter::repeat_n([255, 0, 0, 255].iter(), 32 * 32).flatten());

        // print the test image with the action set to query
        print!("\x1B_Gi=1,f=32,s=32,v=32,a=q;{}\x1B\\", engine::general_purpose::STANDARD.encode(&test_image));
        stdout().flush()?;

        let start_time: Instant = Instant::now();
        let stdin_fd: BorrowedFd = stdin.as_fd();
        let allowed_bytes: [u8; 4] = [0x1B, b'_', b'G', b'\\'];

        let mut buf: Vec<u8> = Vec::<u8>::new();
        let mut stdin_pollfd: [PollFd; 1] = [PollFd::new(&stdin_fd, PollFlags::IN)];

        loop {
            // check for timeout while polling to avoid blocking the main thread
            while poll(&mut stdin_pollfd, Some(&Timespec::default()))? < 1 {
                if start_time.elapsed().as_millis() > 50 {
                    tcsetattr(&stdin, OptionalActions::Now, &old_attributes).context("Failed to update terminal attributes")?;
                    return Ok(false);
                }
            }

            let mut byte: [u8; 1] = [0];
            read(&stdin, &mut byte)?;

            if allowed_bytes.contains(&byte[0]) {
                (&mut buf).push(byte[0]);
            }

            if buf.starts_with(&[0x1B, b'_', b'G']) && buf.ends_with(&[0x1B, b'\\']) {
                tcsetattr(&stdin, OptionalActions::Now, &old_attributes).context("Failed to update terminal attributes")?;
                return Ok(true);
            }
        }
    }
}

impl ImageBackend for KittyBackend {
    fn add_image(&self, config: &DisplayConfig, image: &DynamicImage) -> Result<String> {
        let tty_size: Winsize = tcgetwinsize(std::io::stdin())?;
        let height_ratio: f64 = f64::from(tty_size.ws_row) / f64::from(tty_size.ws_ypixel);

        // resize image to fit the text height with the Lanczos3 algorithm
        let image: DynamicImage = image.resize(u32::MAX, (config.lines.len() as f64 / height_ratio) as u32, FilterType::Lanczos3);
        let image_rows: f64 = height_ratio * f64::from(image.height());

        // convert the image to rgba samples
        let rgba_image: RgbaImage = image.to_rgba8();
        let flat_samples: FlatSamples<&[u8]> = rgba_image.as_flat_samples();
        let raw_image: &[u8] = flat_samples.image_slice().expect("Conversion from image to rgba samples failed");

        assert_eq!(image.width() as usize * image.height() as usize * 4, raw_image.len());

        let encoded_image: String = engine::general_purpose::STANDARD.encode(raw_image); // image data is base64 encoded
        let mut image_data: Vec<u8> = Vec::<u8>::new();

        for chunk in encoded_image.as_bytes().chunks(4096) {
            // send a 4096 byte chunk of base64 encoded rgba image data
            (&mut image_data).extend(format!("\x1B_Gf=32,s={},v={},m=1,a=T;", image.width(), image.height()).as_bytes());
            (&mut image_data).extend(chunk);
            (&mut image_data).extend(b"\x1B\\");
        }

        (&mut image_data).extend(b"\x1B_Gm=0;\x1B\\"); // write empty last chunk
        (&mut image_data).extend(format!("\x1B[{}A", image_rows as u32 - 1).as_bytes()); // move cursor to start of image

        let mut i: usize = 0;

        for line in &config.lines {
            (&mut image_data).extend(format!("\x1B[s{}{}\x1B[u\x1B[1B", " ".repeat(config.margin), line).as_bytes());
            i += 1;
        }

        (&mut image_data).extend(format!("\n\x1B[{}B", config.lines.len().max(image_rows as usize) - i).as_bytes()); // move cursor to end of image
        Ok(String::from_utf8(image_data)?)
    }
}
