// TODO CLI tool, read resolution of term

use std::{
    io::{self, Write},
    path::Path,
    thread,
    time::{Duration, Instant},
};

use clap::{Parser, ValueEnum};
use ffmpeg::software::scaling::{context::Context as ScalingContext, flag::Flags};
use ffmpeg::util::format::pixel::Pixel;
use ffmpeg_next as ffmpeg;

const ASCII_GRADIENT: &[char] = &[
    ' ', '.', ',', '-', '~', ':', ';', '=', '!', '*', '#', '$', '@',
];
// const ASCII_GRADIENT: &[char] = &[' ', '░', '▒', '▓', '█'];
// const ASCII_GRADIENT: &[char] = &[' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum ArtStyle {
    Pixel,
    Ascii,
}

#[derive(Parser)]
struct Args {
    #[arg(short, long)]
    path: String,
    #[arg(short, long, default_value_t = true)]
    keep_ar: bool,
    #[arg(short, long, default_value_t = false)]
    loop_video: bool,
    #[arg(short, long, value_enum, default_value_t =ArtStyle::Pixel)]
    art_style: ArtStyle,
}

struct VideoReader {
    input: ffmpeg::format::context::Input,
    fps: f32,
    dims: (u32, u32),
    decoder: ffmpeg::decoder::Video,
    scaler: ScalingContext,
    stream_index: usize,
    frame: ffmpeg::util::frame::Video,
    rgb_frame: ffmpeg::util::frame::Video,
    loop_video: bool,
}

impl VideoReader {
    fn new(path: &Path, loop_video: bool, term_width: usize, term_height: usize) -> Self {
        ffmpeg::init().unwrap();

        let input = ffmpeg::format::input(path).unwrap();

        let stream = input
            .streams()
            .best(ffmpeg::media::Type::Video)
            .expect("video stream not found");

        let framerate = stream.avg_frame_rate();
        let framerate_f32 = framerate.numerator() as f32 / framerate.denominator() as f32;

        let stream_index = stream.index();

        let codec_context =
            ffmpeg::codec::context::Context::from_parameters(stream.parameters()).unwrap();

        let decoder = codec_context.decoder().video().unwrap();

        let width = decoder.width();
        let height = decoder.height();

        let ar = width as f32 / height as f32;

        let target_width;
        let target_height;

        let prep_term_height = (term_height - 2) * 2;

        dbg!(&term_width, &term_height, &prep_term_height);
        if ar > term_width as f32 / (prep_term_height) as f32 {
            target_height = (term_width as f32 / ar).floor() as u32;
            target_width = term_width as u32;
        } else {
            target_height = prep_term_height as u32;
            target_width = (prep_term_height as f32 * ar).floor() as u32;
        }
        dbg!(
            &target_width,
            (prep_term_height as f32 * ar).floor(),
            &target_height
        );

        let scaler = ScalingContext::get(
            decoder.format(),
            width,
            height,
            Pixel::RGB24,
            target_width,
            target_height.div_euclid(2) * 2,
            Flags::BILINEAR,
        )
        .unwrap();

        Self {
            input,
            fps: framerate_f32,
            dims: (target_width, target_height.div_euclid(2) * 2),
            decoder,
            scaler,
            stream_index,
            frame: ffmpeg::util::frame::Video::empty(),
            rgb_frame: ffmpeg::util::frame::Video::empty(),
            loop_video,
        }
    }

    fn extract_frame_data(&mut self) -> Vec<[u8; 2]> {
        self.scaler.run(&self.frame, &mut self.rgb_frame).unwrap();

        let width = self.rgb_frame.width() as usize;
        let height = self.rgb_frame.height() as usize;
        let stride = self.rgb_frame.stride(0);

        let mut rgb: Vec<[u8; 2]> = Vec::with_capacity(width * height * 3);

        let mut y = 0;
        while y < height {
            let upper = y * stride;
            let lower = (y + 1) * stride;
            let row_upper = &self.rgb_frame.data(0)[upper..upper + width * 3];
            let row_lower = &self.rgb_frame.data(0)[lower..lower + width * 3];
            let dualrow: Vec<[u8; 2]> = row_upper
                .iter()
                .zip(row_lower.iter())
                .map(|(&x, &y)| [x, y])
                .collect();
            rgb.extend(dualrow);
            y += 2
        }

        return rgb;
    }

    fn next_frame(&mut self) -> Option<Vec<[u8; 2]>> {
        let mut packets_iter = self.input.packets();
        loop {
            if let Some((stream, packet)) = packets_iter.next() {
                if stream.index() != self.stream_index {
                    continue;
                }

                self.decoder.send_packet(&packet).unwrap();

                if let Ok(_) = self.decoder.receive_frame(&mut self.frame) {
                    return Some(self.extract_frame_data());
                }
            } else if self.loop_video {
                self.decoder.flush();
                self.input.seek(0, ..).unwrap();
                packets_iter = self.input.packets();
            } else {
                return None;
            }
        }
    }
}

fn render_pixel_art(frame: &Vec<[u8; 2]>, reader: &VideoReader, buf: &mut Vec<u8>) {
    for row in 0..reader.dims.1 / 2 {
        for col in 0..reader.dims.0 {
            let idx = 3 * (row * reader.dims.0 + col);
            let reds = frame.get(idx as usize).unwrap_or_else(|| {
                panic!("Index: {idx} is out of bounds at row {row} and col {col}. ")
            });
            let greens = frame.get((idx + 1) as usize).unwrap_or_else(|| {
                panic!("Index: {idx} is out of bounds at row {row} and col {col}. ")
            });

            let blues = frame.get((idx + 2) as usize).unwrap_or_else(|| {
                panic!("Index: {idx} is out of bounds at row {row} and col {col}. ")
            });

            buf.extend_from_slice(
                format!(
                    "\x1b[48;2;{br};{bg};{bb}m\x1b[38;2;{tr};{tg};{tb}m▀",
                    br = reds[1],
                    bg = greens[1],
                    bb = blues[1],
                    tr = reds[0],
                    tg = greens[0],
                    tb = blues[0],
                )
                .as_bytes(),
            );
        }
        buf.extend_from_slice(b"\x1b[0m\n");
    }
}

fn luminance(r: u8, g: u8, b: u8) -> f32 {
    0.2126 * f32::from(r) + 0.7152 * f32::from(g) + 0.0722 * f32::from(b)
}

fn render_ascii_art(frame: &Vec<[u8; 2]>, reader: &VideoReader, buf: &mut Vec<u8>) {
    for row in 0..reader.dims.1 / 2 {
        for col in 0..reader.dims.0 {
            let idx = 3 * (row * reader.dims.0 + col);
            let reds = frame.get(idx as usize).unwrap_or_else(|| {
                panic!("Index: {idx} is out of bounds at row {row} and col {col}. ")
            });
            let greens = frame.get((idx + 1) as usize).unwrap();
            let blues = frame.get((idx + 2) as usize).unwrap();

            buf.extend_from_slice(
                format!(
                    "\x1b[38;2;{tr};{tg};{tb}m",
                    tr = reds[0],
                    tg = greens[0],
                    tb = blues[0],
                )
                .as_bytes(),
            );
            let mut extend = [0u8; 4];
            buf.extend_from_slice(
                ASCII_GRADIENT[luminance(reds[0], greens[0], blues[0]) as usize
                    * (ASCII_GRADIENT.len() - 1)
                    / 255]
                    .encode_utf8(&mut extend)
                    .as_bytes(),
            );
        }
        buf.extend_from_slice(b"\x1b[0m\n");
    }
}

fn main() {
    let args = Args::parse();

    let term_dims =
        term_size::dimensions().unwrap_or_else(|| panic!("Could not find terminal dimensions"));

    let mut reader = VideoReader::new(
        Path::new(&args.path),
        args.loop_video,
        term_dims.0,
        term_dims.1,
    );

    let frame_duration_micros: Duration =
        Duration::from_micros((1_000_000.0 / reader.fps).floor() as u64);

    let mut out = io::stdout().lock();
    let mut buf: Vec<u8> = Vec::new();
    let start_time = Instant::now();

    // Clear screen and hide cursor
    let _ = out.write_all(b"\x1b[2J\x1b[?25l");

    while let Some(frame) = reader.next_frame() {
        let time = start_time.elapsed();

        buf.extend_from_slice(b"\x1b[J\x1b[H"); // move to start
        match args.art_style {
            ArtStyle::Pixel => render_pixel_art(&frame, &reader, &mut buf),
            ArtStyle::Ascii => render_ascii_art(&frame, &reader, &mut buf),
        }

        let _ = out.write_all(&buf);
        let _ = out.flush();

        buf.clear();

        let sleep_time = frame_duration_micros - (start_time.elapsed() - time);
        if sleep_time.as_micros() > 0 {
            thread::sleep(sleep_time);
        }
    }
}
