use crate::{
    markdown::text_style::{Color, TextStyle},
    terminal::{
        image::printer::{ImageProperties, ImageSpec, PrintImage, PrintImageError, PrintOptions, RegisterImageError},
        printer::{TerminalCommand, TerminalIo},
    },
};
use base64::{Engine, engine::general_purpose::STANDARD};
use image::{AnimationDecoder, Delay, EncodableLayout, ImageReader, RgbaImage, codecs::gif::GifDecoder};
use std::{
    fmt,
    fs::{self, File},
    io::{self, BufReader},
    path::{Path, PathBuf},
    sync::atomic::{AtomicU32, Ordering},
};
use tempfile::{TempDir, tempdir};

const IMAGE_PLACEHOLDER: &str = "\u{10EEEE}";
const DIACRITICS: &[u32] = &[
    0x305, 0x30d, 0x30e, 0x310, 0x312, 0x33d, 0x33e, 0x33f, 0x346, 0x34a, 0x34b, 0x34c, 0x350, 0x351, 0x352, 0x357,
    0x35b, 0x363, 0x364, 0x365, 0x366, 0x367, 0x368, 0x369, 0x36a, 0x36b, 0x36c, 0x36d, 0x36e, 0x36f, 0x483, 0x484,
    0x485, 0x486, 0x487, 0x592, 0x593, 0x594, 0x595, 0x597, 0x598, 0x599, 0x59c, 0x59d, 0x59e, 0x59f, 0x5a0, 0x5a1,
    0x5a8, 0x5a9, 0x5ab, 0x5ac, 0x5af, 0x5c4, 0x610, 0x611, 0x612, 0x613, 0x614, 0x615, 0x616, 0x617, 0x657, 0x658,
    0x659, 0x65a, 0x65b, 0x65d, 0x65e, 0x6d6, 0x6d7, 0x6d8, 0x6d9, 0x6da, 0x6db, 0x6dc, 0x6df, 0x6e0, 0x6e1, 0x6e2,
    0x6e4, 0x6e7, 0x6e8, 0x6eb, 0x6ec, 0x730, 0x732, 0x733, 0x735, 0x736, 0x73a, 0x73d, 0x73f, 0x740, 0x741, 0x743,
    0x745, 0x747, 0x749, 0x74a, 0x7eb, 0x7ec, 0x7ed, 0x7ee, 0x7ef, 0x7f0, 0x7f1, 0x7f3, 0x816, 0x817, 0x818, 0x819,
    0x81b, 0x81c, 0x81d, 0x81e, 0x81f, 0x820, 0x821, 0x822, 0x823, 0x825, 0x826, 0x827, 0x829, 0x82a, 0x82b, 0x82c,
    0x82d, 0x951, 0x953, 0x954, 0xf82, 0xf83, 0xf86, 0xf87, 0x135d, 0x135e, 0x135f, 0x17dd, 0x193a, 0x1a17, 0x1a75,
    0x1a76, 0x1a77, 0x1a78, 0x1a79, 0x1a7a, 0x1a7b, 0x1a7c, 0x1b6b, 0x1b6d, 0x1b6e, 0x1b6f, 0x1b70, 0x1b71, 0x1b72,
    0x1b73, 0x1cd0, 0x1cd1, 0x1cd2, 0x1cda, 0x1cdb, 0x1ce0, 0x1dc0, 0x1dc1, 0x1dc3, 0x1dc4, 0x1dc5, 0x1dc6, 0x1dc7,
    0x1dc8, 0x1dc9, 0x1dcb, 0x1dcc, 0x1dd1, 0x1dd2, 0x1dd3, 0x1dd4, 0x1dd5, 0x1dd6, 0x1dd7, 0x1dd8, 0x1dd9, 0x1dda,
    0x1ddb, 0x1ddc, 0x1ddd, 0x1dde, 0x1ddf, 0x1de0, 0x1de1, 0x1de2, 0x1de3, 0x1de4, 0x1de5, 0x1de6, 0x1dfe, 0x20d0,
    0x20d1, 0x20d4, 0x20d5, 0x20d6, 0x20d7, 0x20db, 0x20dc, 0x20e1, 0x20e7, 0x20e9, 0x20f0, 0x2cef, 0x2cf0, 0x2cf1,
    0x2de0, 0x2de1, 0x2de2, 0x2de3, 0x2de4, 0x2de5, 0x2de6, 0x2de7, 0x2de8, 0x2de9, 0x2dea, 0x2deb, 0x2dec, 0x2ded,
    0x2dee, 0x2def, 0x2df0, 0x2df1, 0x2df2, 0x2df3, 0x2df4, 0x2df5, 0x2df6, 0x2df7, 0x2df8, 0x2df9, 0x2dfa, 0x2dfb,
    0x2dfc, 0x2dfd, 0x2dfe, 0x2dff, 0xa66f, 0xa67c, 0xa67d, 0xa6f0, 0xa6f1, 0xa8e0, 0xa8e1, 0xa8e2, 0xa8e3, 0xa8e4,
    0xa8e5, 0xa8e6, 0xa8e7, 0xa8e8, 0xa8e9, 0xa8ea, 0xa8eb, 0xa8ec, 0xa8ed, 0xa8ee, 0xa8ef, 0xa8f0, 0xa8f1, 0xaab0,
    0xaab2, 0xaab3, 0xaab7, 0xaab8, 0xaabe, 0xaabf, 0xaac1, 0xfe20, 0xfe21, 0xfe22, 0xfe23, 0xfe24, 0xfe25, 0xfe26,
    0x10a0f, 0x10a38, 0x1d185, 0x1d186, 0x1d187, 0x1d188, 0x1d189, 0x1d1aa, 0x1d1ab, 0x1d1ac, 0x1d1ad, 0x1d242,
    0x1d243, 0x1d244,
];

enum GenericResource<B> {
    Image(B),
    Gif(Vec<GifFrame<B>>),
}

type RawResource = GenericResource<RgbaImage>;

impl RawResource {
    fn into_memory_resource(self) -> KittyImage {
        match self {
            Self::Image(image) => KittyImage {
                dimensions: image.dimensions(),
                resource: GenericResource::Image(KittyBuffer::Memory(image.into_raw())),
            },
            Self::Gif(frames) => {
                let dimensions = frames[0].buffer.dimensions();
                let frames = frames
                    .into_iter()
                    .map(|frame| GifFrame { delay: frame.delay, buffer: KittyBuffer::Memory(frame.buffer.into_raw()) })
                    .collect();
                let resource = GenericResource::Gif(frames);
                KittyImage { dimensions, resource }
            }
        }
    }
}

pub(crate) struct KittyImage {
    dimensions: (u32, u32),
    resource: GenericResource<KittyBuffer>,
}

impl KittyImage {
    pub(crate) fn as_rgba8(&self) -> RgbaImage {
        let first_frame = match &self.resource {
            GenericResource::Image(buffer) => buffer,
            GenericResource::Gif(gif_frames) => &gif_frames[0].buffer,
        };
        let buffer = match first_frame {
            KittyBuffer::Filesystem(path) => {
                let Ok(contents) = fs::read(path) else {
                    return RgbaImage::default();
                };
                contents
            }
            KittyBuffer::Memory(buffer) => buffer.clone(),
        };
        RgbaImage::from_raw(self.dimensions.0, self.dimensions.1, buffer).unwrap_or_default()
    }
}

impl ImageProperties for KittyImage {
    fn dimensions(&self) -> (u32, u32) {
        self.dimensions
    }
}

enum KittyBuffer {
    Filesystem(PathBuf),
    Memory(Vec<u8>),
}

impl Drop for KittyBuffer {
    fn drop(&mut self) {
        if let Self::Filesystem(path) = self {
            let _ = fs::remove_file(path);
        }
    }
}

struct GifFrame<T> {
    delay: Delay,
    buffer: T,
}

pub struct KittyPrinter {
    mode: KittyMode,
    tmux: bool,
    base_directory: TempDir,
    next: AtomicU32,
}

impl KittyPrinter {
    pub(crate) fn new(mode: KittyMode, tmux: bool) -> io::Result<Self> {
        let base_directory = tempdir()?;
        Ok(Self { mode, tmux, base_directory, next: Default::default() })
    }

    fn allocate_tempfile(&self) -> PathBuf {
        let file_number = self.next.fetch_add(1, Ordering::AcqRel);
        self.base_directory.path().join(file_number.to_string())
    }

    fn persist_image(&self, image: RgbaImage) -> io::Result<KittyImage> {
        let path = self.allocate_tempfile();
        fs::write(&path, image.as_bytes())?;

        let buffer = KittyBuffer::Filesystem(path);
        let resource = KittyImage { dimensions: image.dimensions(), resource: GenericResource::Image(buffer) };
        Ok(resource)
    }

    fn persist_gif(&self, frames: Vec<GifFrame<RgbaImage>>) -> io::Result<KittyImage> {
        let mut persisted_frames = Vec::new();
        let mut dimensions = (0, 0);
        for frame in frames {
            let path = self.allocate_tempfile();
            fs::write(&path, frame.buffer.as_bytes())?;
            dimensions = frame.buffer.dimensions();

            let frame = GifFrame { delay: frame.delay, buffer: KittyBuffer::Filesystem(path) };
            persisted_frames.push(frame);
        }
        Ok(KittyImage { dimensions, resource: GenericResource::Gif(persisted_frames) })
    }

    fn persist_resource(&self, resource: RawResource) -> io::Result<KittyImage> {
        match resource {
            RawResource::Image(image) => self.persist_image(image),
            RawResource::Gif(frames) => self.persist_gif(frames),
        }
    }

    fn generate_image_id() -> u32 {
        fastrand::u32(1..u32::MAX)
    }

    fn print_image<T>(
        &self,
        dimensions: (u32, u32),
        buffer: &KittyBuffer,
        terminal: &mut T,
        print_options: &PrintOptions,
    ) -> Result<(), PrintImageError>
    where
        T: TerminalIo,
    {
        let mut options = vec![
            ControlOption::Format(ImageFormat::Rgba),
            ControlOption::Action(Action::TransmitAndDisplay),
            ControlOption::Width(dimensions.0),
            ControlOption::Height(dimensions.1),
            ControlOption::Columns(print_options.columns),
            ControlOption::Rows(print_options.rows),
            ControlOption::ZIndex(print_options.z_index),
            ControlOption::Quiet(2),
        ];
        let mut image_id = 0;
        if self.tmux {
            image_id = Self::generate_image_id();
            options.extend([ControlOption::UnicodePlaceholder, ControlOption::ImageId(image_id)]);
        }

        match &buffer {
            KittyBuffer::Filesystem(path) => self.print_local(options, path, terminal)?,
            KittyBuffer::Memory(buffer) => self.print_remote(options, buffer, terminal, false)?,
        };
        if self.tmux {
            self.print_unicode_placeholders(terminal, print_options, image_id)?;
        }

        Ok(())
    }

    fn print_gif<T>(
        &self,
        dimensions: (u32, u32),
        frames: &[GifFrame<KittyBuffer>],
        terminal: &mut T,
        print_options: &PrintOptions,
    ) -> Result<(), PrintImageError>
    where
        T: TerminalIo,
    {
        let image_id = Self::generate_image_id();
        for (frame_id, frame) in frames.iter().enumerate() {
            let (num, denom) = frame.delay.numer_denom_ms();
            // default to 100ms in case somehow the denominator is 0
            let delay = num.checked_div(denom).unwrap_or(100);
            let mut options = vec![
                ControlOption::Format(ImageFormat::Rgba),
                ControlOption::ImageId(image_id),
                ControlOption::Width(dimensions.0),
                ControlOption::Height(dimensions.1),
                ControlOption::ZIndex(print_options.z_index),
                ControlOption::Quiet(2),
            ];
            if frame_id == 0 {
                options.extend([
                    ControlOption::Action(Action::TransmitAndDisplay),
                    ControlOption::Columns(print_options.columns),
                    ControlOption::Rows(print_options.rows),
                ]);
                if self.tmux {
                    options.push(ControlOption::UnicodePlaceholder);
                }
            } else {
                options.extend([ControlOption::Action(Action::TransmitFrame), ControlOption::Delay(delay)]);
            }

            let is_frame = frame_id > 0;
            match &frame.buffer {
                KittyBuffer::Filesystem(path) => self.print_local(options, path, terminal)?,
                KittyBuffer::Memory(buffer) => self.print_remote(options, buffer, terminal, is_frame)?,
            };

            if frame_id == 0 {
                let options = &[
                    ControlOption::Action(Action::Animate),
                    ControlOption::ImageId(image_id),
                    ControlOption::FrameId(1),
                    ControlOption::Loops(1),
                ];
                let command = self.make_command(options, "").to_string();
                terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?;
            } else if frame_id == 1 {
                let options = &[
                    ControlOption::Action(Action::Animate),
                    ControlOption::ImageId(image_id),
                    ControlOption::FrameId(1),
                    ControlOption::AnimationState(2),
                ];
                let command = self.make_command(options, "").to_string();
                terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?;
            }
        }
        if self.tmux {
            self.print_unicode_placeholders(terminal, print_options, image_id)?;
        }
        let options = &[
            ControlOption::Action(Action::Animate),
            ControlOption::ImageId(image_id),
            ControlOption::FrameId(1),
            ControlOption::AnimationState(3),
            ControlOption::Loops(1),
            ControlOption::Quiet(2),
        ];
        let command = self.make_command(options, "").to_string();
        terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?;
        Ok(())
    }

    fn make_command<'a, P>(&self, options: &'a [ControlOption], payload: P) -> ControlCommand<'a, P> {
        ControlCommand { options, payload, tmux: self.tmux }
    }

    fn print_local<T>(
        &self,
        mut options: Vec<ControlOption>,
        path: &Path,
        terminal: &mut T,
    ) -> Result<(), PrintImageError>
    where
        T: TerminalIo,
    {
        let Some(path) = path.to_str() else {
            return Err(PrintImageError::other("path is not valid utf8"));
        };
        let encoded_path = STANDARD.encode(path);
        options.push(ControlOption::Medium(TransmissionMedium::LocalFile));

        let command = self.make_command(&options, &encoded_path).to_string();
        terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?;
        Ok(())
    }

    fn print_remote<T>(
        &self,
        mut options: Vec<ControlOption>,
        frame: &[u8],
        terminal: &mut T,
        is_frame: bool,
    ) -> Result<(), PrintImageError>
    where
        T: TerminalIo,
    {
        options.push(ControlOption::Medium(TransmissionMedium::Direct));

        let payload = STANDARD.encode(frame);
        let chunk_size = 4096;
        let mut index = 0;
        while index < payload.len() {
            let start = index;
            let end = payload.len().min(start + chunk_size);
            index = end;

            let more = end != payload.len();
            options.push(ControlOption::MoreData(more));

            let payload = &payload[start..end];
            let command = self.make_command(&options, payload).to_string();
            terminal.execute(&TerminalCommand::PrintText { content: &command, style: Default::default() })?;

            options.clear();
            if is_frame {
                options.push(ControlOption::Action(Action::TransmitFrame));
            }
        }
        Ok(())
    }

    fn print_unicode_placeholders<T>(
        &self,
        terminal: &mut T,
        options: &PrintOptions,
        image_id: u32,
    ) -> Result<(), PrintImageError>
    where
        T: TerminalIo,
    {
        let color = Color::new((image_id >> 16) as u8, (image_id >> 8) as u8, image_id as u8);
        let style = TextStyle::default().fg_color(color);
        if options.rows.max(options.columns) >= DIACRITICS.len() as u16 {
            return Err(PrintImageError::other("image is too large to fit in tmux"));
        }

        let last_byte = char::from_u32(DIACRITICS[(image_id >> 24) as usize]).unwrap();
        for row in 0..options.rows {
            let row_diacritic = char::from_u32(DIACRITICS[row as usize]).unwrap();
            for column in 0..options.columns {
                let column_diacritic = char::from_u32(DIACRITICS[column as usize]).unwrap();
                let content = format!("{IMAGE_PLACEHOLDER}{row_diacritic}{column_diacritic}{last_byte}");
                terminal.execute(&TerminalCommand::PrintText { content: &content, style })?;
            }
            if row != options.rows - 1 {
                terminal.execute(&TerminalCommand::MoveDown(1))?;
            }
            terminal.execute(&TerminalCommand::MoveLeft(options.columns))?;
        }
        Ok(())
    }

    fn load_raw_resource(path: &Path) -> Result<RawResource, RegisterImageError> {
        let file = File::open(path)?;
        if path.extension().unwrap_or_default() == "gif" {
            let decoder = GifDecoder::new(BufReader::new(file))?;
            let mut frames = Vec::new();
            for frame in decoder.into_frames() {
                let frame = frame?;
                let frame = GifFrame { delay: frame.delay(), buffer: frame.into_buffer() };
                frames.push(frame);
            }
            Ok(RawResource::Gif(frames))
        } else {
            let reader = ImageReader::new(BufReader::new(file)).with_guessed_format()?;
            let image = reader.decode()?;
            Ok(RawResource::Image(image.into_rgba8()))
        }
    }
}

impl PrintImage for KittyPrinter {
    type Image = KittyImage;

    fn register(&self, spec: ImageSpec) -> Result<Self::Image, RegisterImageError> {
        let image = match spec {
            ImageSpec::Generated(image) => RawResource::Image(image.into_rgba8()),
            ImageSpec::Filesystem(path) => Self::load_raw_resource(&path)?,
        };
        let resource = match &self.mode {
            KittyMode::Local => self.persist_resource(image)?,
            KittyMode::Remote => image.into_memory_resource(),
        };
        Ok(resource)
    }

    fn print<T>(&self, image: &Self::Image, options: &PrintOptions, terminal: &mut T) -> Result<(), PrintImageError>
    where
        T: TerminalIo,
    {
        match &image.resource {
            GenericResource::Image(resource) => self.print_image(image.dimensions, resource, terminal, options)?,
            GenericResource::Gif(frames) => self.print_gif(image.dimensions, frames, terminal, options)?,
        };
        Ok(())
    }
}

#[derive(Clone, Debug)]
pub enum KittyMode {
    Local,
    Remote,
}

pub(crate) struct ControlCommand<'a, D> {
    pub(crate) options: &'a [ControlOption],
    pub(crate) payload: D,
    pub(crate) tmux: bool,
}

impl<D: fmt::Display> fmt::Display for ControlCommand<'_, D> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.tmux {
            write!(f, "\x1bPtmux;\x1b")?;
        }
        write!(f, "\x1b_G")?;
        for (index, option) in self.options.iter().enumerate() {
            if index > 0 {
                write!(f, ",")?;
            }
            write!(f, "{option}")?;
        }
        write!(f, ";{}", &self.payload)?;
        if self.tmux {
            write!(f, "\x1b\x1b\\\x1b\\")?;
        } else {
            write!(f, "\x1b\\")?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone)]
pub(crate) enum ControlOption {
    Action(Action),
    Format(ImageFormat),
    Medium(TransmissionMedium),
    Width(u32),
    Height(u32),
    Columns(u16),
    Rows(u16),
    MoreData(bool),
    ImageId(u32),
    FrameId(u32),
    Delay(u32),
    AnimationState(u32),
    Loops(u32),
    Quiet(u32),
    ZIndex(i32),
    UnicodePlaceholder,
}

impl fmt::Display for ControlOption {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use ControlOption::*;
        match self {
            Action(action) => write!(f, "a={action}"),
            Format(format) => write!(f, "f={format}"),
            Medium(medium) => write!(f, "t={medium}"),
            Width(width) => write!(f, "s={width}"),
            Height(height) => write!(f, "v={height}"),
            Columns(columns) => write!(f, "c={columns}"),
            Rows(rows) => write!(f, "r={rows}"),
            MoreData(true) => write!(f, "m=1"),
            MoreData(false) => write!(f, "m=0"),
            ImageId(id) => write!(f, "i={id}"),
            FrameId(id) => write!(f, "r={id}"),
            Delay(delay) => write!(f, "z={delay}"),
            AnimationState(state) => write!(f, "s={state}"),
            Loops(count) => write!(f, "v={count}"),
            Quiet(option) => write!(f, "q={option}"),
            ZIndex(index) => write!(f, "z={index}"),
            UnicodePlaceholder => write!(f, "U=1"),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) enum ImageFormat {
    Rgba,
}

impl fmt::Display for ImageFormat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use ImageFormat::*;
        let value = match self {
            Rgba => 32,
        };
        write!(f, "{value}")
    }
}

#[derive(Debug, Clone)]
pub(crate) enum TransmissionMedium {
    Direct,
    LocalFile,
}

impl fmt::Display for TransmissionMedium {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use TransmissionMedium::*;
        let value = match self {
            Direct => 'd',
            LocalFile => 'f',
        };
        write!(f, "{value}")
    }
}

#[derive(Debug, Clone)]
pub(crate) enum Action {
    Animate,
    TransmitAndDisplay,
    TransmitFrame,
    Query,
}

impl fmt::Display for Action {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use Action::*;
        let value = match self {
            Animate => 'a',
            TransmitAndDisplay => 'T',
            TransmitFrame => 'f',
            Query => 'q',
        };
        write!(f, "{value}")
    }
}
